diff --git a/codegen/cuekind/def.cue b/codegen/cuekind/def.cue index 53453b01..4706c2f7 100644 --- a/codegen/cuekind/def.cue +++ b/codegen/cuekind/def.cue @@ -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])$" @@ -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)) diff --git a/codegen/cuekind/generators_test.go b/codegen/cuekind/generators_test.go index e24e1658..7cee3b20 100644 --- a/codegen/cuekind/generators_test.go +++ b/codegen/cuekind/generators_test.go @@ -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") }) @@ -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") }) diff --git a/codegen/cuekind/testing/testkind.cue b/codegen/cuekind/testing/testkind.cue index 25d65ae9..d20f4eab 100644 --- a/codegen/cuekind/testing/testkind.cue +++ b/codegen/cuekind/testing/testkind.cue @@ -30,6 +30,13 @@ testKind: { } } mutation: operations: ["create","update"] + additionalPrinterColumns: [ + { + jsonPath: ".spec.stringField" + name: "STRING FIELD" + type: "string" + } + ] } } } \ No newline at end of file diff --git a/codegen/jennies/crd.go b/codegen/jennies/crd.go index f51a39e8..222c3b16 100644 --- a/codegen/jennies/crd.go +++ b/codegen/jennies/crd.go @@ -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{}{} diff --git a/codegen/kind.go b/codegen/kind.go index b2161c50..30b4c1fa 100644 --- a/codegen/kind.go +++ b/codegen/kind.go @@ -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 diff --git a/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.json.txt b/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.json.txt index 0e1b3c34..bcb4c816 100644 --- a/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.json.txt +++ b/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.json.txt @@ -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"}} \ No newline at end of file +{"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"}} \ No newline at end of file diff --git a/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.yaml.txt b/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.yaml.txt index 9d680da6..22387157 100644 --- a/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.yaml.txt +++ b/codegen/testing/golden_generated/crd/testkind.test.ext.grafana.com.yaml.txt @@ -125,6 +125,10 @@ spec: type: object subresources: status: {} + additionalPrinterColumns: + - name: STRING FIELD + type: string + jsonPath: .spec.stringField names: kind: TestKind plural: testkinds diff --git a/docs/custom-kinds/writing-kinds.md b/docs/custom-kinds/writing-kinds.md index 3eee7354..9adaf901 100644 --- a/docs/custom-kinds/writing-kinds.md +++ b/docs/custom-kinds/writing-kinds.md @@ -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/). diff --git a/k8s/manager.go b/k8s/manager.go index 0d40105e..e6ef75ce 100644 --- a/k8s/manager.go +++ b/k8s/manager.go @@ -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 @@ -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()