Skip to content

Commit

Permalink
Teach codegen to accept and generate additionalPrinterColumns (#408)
Browse files Browse the repository at this point in the history
This will get passed through to the CRDs.

---------

Co-authored-by: Austin Pond <IfSentient@users.noreply.github.com>
  • Loading branch information
bcotton and IfSentient authored Sep 17, 2024
1 parent 32d67a0 commit eb9af02
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 16 deletions.
23 changes: 23 additions & 0 deletions codegen/cuekind/def.cue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ Schema: {
operations: [...string]
}

#AdditionalPrinterColumns: {
// name is a human readable name for the column.
name: string
// type is an OpenAPI type definition for this column.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details.
type: string
// format is an optional OpenAPI type definition for this column. The 'name' format is applied
// to the primary identifier column to assist in clients identifying column is the resource name.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details.
format?: string
// description is a human readable description of this column.
description?: string
// priority is an integer defining the relative importance of this column compared to others. Lower
// numbers are considered higher priority. Columns that may be omitted in limited space scenarios
// should be given a priority greater than 0.
priority?: int32
// jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against
// each custom resource to produce the value for this column.
jsonPath: string
}

// Kind represents an arbitrary kind which can be used for code generation
Kind: S={
kind: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$"
Expand Down Expand Up @@ -144,6 +165,8 @@ Kind: S={
selectableFields: [...string]
validation: #AdmissionCapability | *S.apiResource.validation
mutation: #AdmissionCapability | *S.apiResource.mutation
// additionalPrinterColumns is a list of additional columns to be printed in kubectl output
additionalPrinterColumns?: [...#AdditionalPrinterColumns]
}
}
machineName: strings.ToLower(strings.Replace(S.kind, "-", "_", -1))
Expand Down
6 changes: 3 additions & 3 deletions codegen/cuekind/generators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ func TestCRDGenerator(t *testing.T) {

parser, err := NewParser()
require.Nil(t, err)
kinds, err := parser.Parse(os.DirFS(TestCUEDirectory), "customKind")
kinds, err := parser.Parse(os.DirFS(TestCUEDirectory), "testKind", "customKind")
require.Nil(t, err)

t.Run("JSON", func(t *testing.T) {
files, err := CRDGenerator(json.Marshal, "json").Generate(kinds...)
require.Nil(t, err)
// Check number of files generated
assert.Len(t, files, 1)
assert.Len(t, files, 2)
// Check content against the golden files
compareToGolden(t, files, "crd")
})
Expand All @@ -40,7 +40,7 @@ func TestCRDGenerator(t *testing.T) {
files, err := CRDGenerator(yaml.Marshal, "yaml").Generate(kinds...)
require.Nil(t, err)
// Check number of files generated
assert.Len(t, files, 1)
assert.Len(t, files, 2)
// Check content against the golden files
compareToGolden(t, files, "crd")
})
Expand Down
7 changes: 7 additions & 0 deletions codegen/cuekind/testing/testkind.cue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ testKind: {
}
}
mutation: operations: ["create","update"]
additionalPrinterColumns: [
{
jsonPath: ".spec.stringField"
name: "STRING FIELD"
type: "string"
}
]
}
}
}
15 changes: 15 additions & 0 deletions codegen/jennies/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ func KindVersionToCRDSpecVersion(kv codegen.KindVersion, kindName string, stored
def.SelectableFields = sf
}

if len(kv.AdditionalPrinterColumns) > 0 {
apc := make([]k8s.CustomResourceDefinitionAdditionalPrinterColumn, len(kv.AdditionalPrinterColumns))
for i, col := range kv.AdditionalPrinterColumns {
apc[i] = k8s.CustomResourceDefinitionAdditionalPrinterColumn{
Name: col.Name,
Type: col.Type,
Format: col.Format,
Description: col.Description,
Priority: col.Priority,
JSONPath: col.JSONPath,
}
}
def.AdditionalPrinterColumns = apc
}

for k := range props {
if k != "spec" {
def.Subresources[k] = struct{}{}
Expand Down
22 changes: 16 additions & 6 deletions codegen/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,26 @@ type KindCodegenProperties struct {
Backend bool `json:"backend"`
}

type AdditionalPrinterColumn struct {
Name string `json:"name"`
Type string `json:"type"`
Format *string `json:"format,omitempty"`
Description *string `json:"description,omitempty"`
Priority *int32 `json:"priority"`
JSONPath string `json:"jsonPath"`
}

type KindVersion struct {
Version string `json:"version"`
// Schema is the CUE schema for the version
// This should eventually be changed to JSONSchema/OpenAPI(/AST?)
Schema cue.Value `json:"schema"` // TODO: this should eventually be OpenAPI/JSONSchema (ast or bytes?)
Codegen KindCodegenProperties `json:"codegen"`
Served bool `json:"served"`
SelectableFields []string `json:"selectableFields"`
Validation KindAdmissionCapability `json:"validation"`
Mutation KindAdmissionCapability `json:"mutation"`
Schema cue.Value `json:"schema"` // TODO: this should eventually be OpenAPI/JSONSchema (ast or bytes?)
Codegen KindCodegenProperties `json:"codegen"`
Served bool `json:"served"`
SelectableFields []string `json:"selectableFields"`
Validation KindAdmissionCapability `json:"validation"`
Mutation KindAdmissionCapability `json:"mutation"`
AdditionalPrinterColumns []AdditionalPrinterColumn `json:"additionalPrinterColumns"`
}

// AnyKind is a simple implementation of Kind
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind":"CustomResourceDefinition","apiVersion":"apiextensions.k8s.io/v1","metadata":{"name":"testkinds.test.ext.grafana.com"},"spec":{"group":"test.ext.grafana.com","versions":[{"name":"v1","served":true,"storage":true,"schema":{"openAPIV3Schema":{"properties":{"spec":{"properties":{"stringField":{"type":"string"}},"required":["stringField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}},"required":["spec"],"type":"object"}},"subresources":{"status":{}}},{"name":"v2","served":true,"storage":false,"schema":{"openAPIV3Schema":{"properties":{"spec":{"properties":{"intField":{"format":"int64","type":"integer"},"stringField":{"type":"string"},"timeField":{"format":"date-time","type":"string"}},"required":["stringField","intField","timeField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}},"required":["spec"],"type":"object"}},"subresources":{"status":{}}}],"names":{"kind":"TestKind","plural":"testkinds"},"scope":"Namespaced"}}
{"kind":"CustomResourceDefinition","apiVersion":"apiextensions.k8s.io/v1","metadata":{"name":"testkinds.test.ext.grafana.com"},"spec":{"group":"test.ext.grafana.com","versions":[{"name":"v1","served":true,"storage":true,"schema":{"openAPIV3Schema":{"properties":{"spec":{"properties":{"stringField":{"type":"string"}},"required":["stringField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}},"required":["spec"],"type":"object"}},"subresources":{"status":{}}},{"name":"v2","served":true,"storage":false,"schema":{"openAPIV3Schema":{"properties":{"spec":{"properties":{"intField":{"format":"int64","type":"integer"},"stringField":{"type":"string"},"timeField":{"format":"date-time","type":"string"}},"required":["stringField","intField","timeField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}},"required":["spec"],"type":"object"}},"subresources":{"status":{}},"additionalPrinterColumns":[{"name":"STRING FIELD","type":"string","jsonPath":".spec.stringField"}]}],"names":{"kind":"TestKind","plural":"testkinds"},"scope":"Namespaced"}}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ spec:
type: object
subresources:
status: {}
additionalPrinterColumns:
- name: STRING FIELD
type: string
jsonPath: .spec.stringField
names:
kind: TestKind
plural: testkinds
Expand Down
30 changes: 30 additions & 0 deletions docs/custom-kinds/writing-kinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,36 @@ foo: {

You can define further, more complex validation and admission control via your operator using admission webhooks, see [Admission Control](../admission-control.md).

### Custom columns when using `kubectl`. aka `additionalPrinterColumns`

The `kind` format allows for configuring the `additionalPrinterColumns` parameter on a [CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns). The format is the same as a CRD, and you add this config as part of "version", next to the `schema`:

```cue
myKind: {
kind: "MyKind"
group: "mygroup"
current: "v1"
[...]
versions: {
"v1": {
schema: {
spec: {
foo: string
}
}
additionalPrinterColumns: [
{
name: "FOO"
type: "string"
jsonPath: ".spec.foo"
}
]
}
}
}
```

### Examples

Example complex schemas used for codegen testing can be found in the [cuekind codegen testing directory](../../codegen/cuekind/testing/).
Expand Down
24 changes: 18 additions & 6 deletions k8s/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,13 @@ type CustomResourceDefinitionSpec struct {

// CustomResourceDefinitionSpecVersion is the representation of a specific version of a CRD, as part of the overall spec
type CustomResourceDefinitionSpecVersion struct {
Name string `json:"name" yaml:"name"`
Served bool `json:"served" yaml:"served"`
Storage bool `json:"storage" yaml:"storage"`
Schema map[string]any `json:"schema" yaml:"schema"`
Subresources map[string]any `json:"subresources,omitempty" yaml:"subresources,omitempty"`
SelectableFields []CustomResourceDefinitionSelectableField `json:"selectableFields,omitempty" yaml:"selectableFields,omitempty"`
Name string `json:"name" yaml:"name"`
Served bool `json:"served" yaml:"served"`
Storage bool `json:"storage" yaml:"storage"`
Schema map[string]any `json:"schema" yaml:"schema"`
Subresources map[string]any `json:"subresources,omitempty" yaml:"subresources,omitempty"`
SelectableFields []CustomResourceDefinitionSelectableField `json:"selectableFields,omitempty" yaml:"selectableFields,omitempty"`
AdditionalPrinterColumns []CustomResourceDefinitionAdditionalPrinterColumn `json:"additionalPrinterColumns,omitempty" yaml:"additionalPrinterColumns,omitempty"`
}

// CustomResourceDefinitionSpecNames is the struct representing the names (kind and plural) of a kubernetes CRD
Expand All @@ -353,6 +354,17 @@ type CustomResourceDefinitionSelectableField struct {
JSONPath string `json:"jsonPath" yaml:"jsonPath"`
}

// CustomResourceDefinitionAdditionalPrinterColumn is the struct representing an additional printer column in a kubernetes CRD.
// This is a copy of https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1#CustomResourceDefinitionAdditionalPrinterColumn
type CustomResourceDefinitionAdditionalPrinterColumn struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Format *string `json:"format,omitempty" yaml:"format,omitempty"`
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
Priority *int32 `json:"priority,omitempty" yaml:"priority,omitempty"`
JSONPath string `json:"jsonPath" yaml:"jsonPath"`
}

// DeepCopyObject is an implementation of the receiver method required for implementing runtime.Object.
func DeepCopyObject(in any) runtime.Object {
val := reflect.ValueOf(in).Elem()
Expand Down

0 comments on commit eb9af02

Please sign in to comment.