diff --git a/Makefile b/Makefile index 1b52cbe..5a41f65 100644 --- a/Makefile +++ b/Makefile @@ -131,12 +131,14 @@ webhook: controller-gen yq crds: controller-gen yq # kamaji chart $(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 0)' > ./charts/kamaji/crds/kamaji.clastix.io_datastores.yaml - $(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 1)' > ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml + $(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 1)' > ./charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml + $(CONTROLLER_GEN) crd webhook paths="./..." output:stdout | $(YQ) 'select(documentIndex == 2)' > ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml $(YQ) -i '. *n load("./charts/kamaji/controller-gen/crd-conversion.yaml")' ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml # kamaji-crds chart cp ./charts/kamaji/controller-gen/crd-conversion.yaml ./charts/kamaji-crds/hack/crd-conversion.yaml $(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_datastores.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_datastores_spec.yaml $(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml + $(YQ) '.spec' ./charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml > ./charts/kamaji-crds/hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml $(YQ) -i '.conversion.webhook.clientConfig.service.name = "{{ .Values.kamajiService }}"' ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml $(YQ) -i '.conversion.webhook.clientConfig.service.namespace = "{{ .Values.kamajiNamespace }}"' ./charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml diff --git a/PROJECT b/PROJECT index 3ef2704..b062ebd 100644 --- a/PROJECT +++ b/PROJECT @@ -7,6 +7,15 @@ plugins: projectName: operator repo: github.com/clastix/kamaji resources: +- api: + crdVersion: v1 + namespaced: false + controller: true + domain: clastix.io + group: kamaji + kind: KubeconfigGenerator + path: github.com/clastix/kamaji/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/kubeconfiggenerator_types.go b/api/v1alpha1/kubeconfiggenerator_types.go new file mode 100644 index 0000000..81e2c51 --- /dev/null +++ b/api/v1alpha1/kubeconfiggenerator_types.go @@ -0,0 +1,91 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + ManagedByLabel = "kamaji.clastix.io/managed-by" + ManagedForLabel = "kamaji.clastix.io/managed-for" +) + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" +//+kubebuilder:metadata:annotations={"cert-manager.io/inject-ca-from=kamaji-system/kamaji-serving-cert"} +//+kubebuilder:resource:scope=Cluster,shortName=kc,categories=kamaji + +// KubeconfigGenerator is the Schema for the kubeconfiggenerators API. +type KubeconfigGenerator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KubeconfigGeneratorSpec `json:"spec,omitempty"` + Status KubeconfigGeneratorStatus `json:"status,omitempty"` +} + +// CompoundValue allows defining a static, or a dynamic value. +// Options are mutually exclusive, just one should be picked up. +// +kubebuilder:validation:XValidation:rule="(has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))",message="Either stringValue or fromDefinition must be set, but not both." +type CompoundValue struct { + // StringValue is a static string value. + StringValue string `json:"stringValue,omitempty"` + // FromDefinition is used to generate a dynamic value, + // it uses the dot notation to access fields from the referenced TenantControlPlane object: + // e.g.: metadata.name + FromDefinition string `json:"fromDefinition,omitempty"` +} + +type KubeconfigGeneratorSpec struct { + // NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects. + NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` + // TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator. + TenantControlPlaneSelector metav1.LabelSelector `json:"tenantControlPlaneSelector,omitempty"` + // Groups is resolved a set of strings used to assign the x509 organisations field. + // It will be recognised by Kubernetes as user groups. + Groups []CompoundValue `json:"groups,omitempty"` + // User resolves to a string to identify the client, assigned to the x509 Common Name field. + User CompoundValue `json:"user"` + // ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator. + // The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`. + //+kubebuilder:default="admin.svc" + ControlPlaneEndpointFrom string `json:"controlPlaneEndpointFrom,omitempty"` +} + +type KubeconfigGeneratorStatusError struct { + // Resource is the Namespaced name of the errored resource. + //+kubebuilder:validation:Required + Resource string `json:"resource"` + // Message is the error message recorded upon the last generator run. + //+kubebuilder:validation:Required + Message string `json:"message"` +} + +// KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator. +type KubeconfigGeneratorStatus struct { + // Resources is the sum of targeted TenantControlPlane objects. + //+kubebuilder:default=0 + Resources int `json:"resources"` + // AvailableResources is the sum of successfully generated resources. + // In case of a different value compared to Resources, check the field errors. + //+kubebuilder:default=0 + AvailableResources int `json:"availableResources"` + // Errors is the list of failed kubeconfig generations. + Errors []KubeconfigGeneratorStatusError `json:"errors,omitempty"` +} + +//+kubebuilder:object:root=true + +// KubeconfigGeneratorList contains a list of TenantControlPlane. +type KubeconfigGeneratorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []KubeconfigGenerator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&KubeconfigGenerator{}, &KubeconfigGeneratorList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3f9b524..b9efa16 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -289,6 +289,21 @@ func (in *ClientCertificate) DeepCopy() *ClientCertificate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompoundValue) DeepCopyInto(out *CompoundValue) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompoundValue. +func (in *CompoundValue) DeepCopy() *CompoundValue { + if in == nil { + return nil + } + out := new(CompoundValue) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ContentRef) DeepCopyInto(out *ContentRef) { *out = *in @@ -951,6 +966,123 @@ func (in *KubeadmPhasesStatus) DeepCopy() *KubeadmPhasesStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeconfigGenerator) DeepCopyInto(out *KubeconfigGenerator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGenerator. +func (in *KubeconfigGenerator) DeepCopy() *KubeconfigGenerator { + if in == nil { + return nil + } + out := new(KubeconfigGenerator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KubeconfigGenerator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeconfigGeneratorList) DeepCopyInto(out *KubeconfigGeneratorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]KubeconfigGenerator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorList. +func (in *KubeconfigGeneratorList) DeepCopy() *KubeconfigGeneratorList { + if in == nil { + return nil + } + out := new(KubeconfigGeneratorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KubeconfigGeneratorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeconfigGeneratorSpec) DeepCopyInto(out *KubeconfigGeneratorSpec) { + *out = *in + in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) + in.TenantControlPlaneSelector.DeepCopyInto(&out.TenantControlPlaneSelector) + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]CompoundValue, len(*in)) + copy(*out, *in) + } + out.User = in.User +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorSpec. +func (in *KubeconfigGeneratorSpec) DeepCopy() *KubeconfigGeneratorSpec { + if in == nil { + return nil + } + out := new(KubeconfigGeneratorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeconfigGeneratorStatus) DeepCopyInto(out *KubeconfigGeneratorStatus) { + *out = *in + if in.Errors != nil { + in, out := &in.Errors, &out.Errors + *out = make([]KubeconfigGeneratorStatusError, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorStatus. +func (in *KubeconfigGeneratorStatus) DeepCopy() *KubeconfigGeneratorStatus { + if in == nil { + return nil + } + out := new(KubeconfigGeneratorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeconfigGeneratorStatusError) DeepCopyInto(out *KubeconfigGeneratorStatusError) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeconfigGeneratorStatusError. +func (in *KubeconfigGeneratorStatusError) DeepCopy() *KubeconfigGeneratorStatusError { + if in == nil { + return nil + } + out := new(KubeconfigGeneratorStatusError) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubeconfigStatus) DeepCopyInto(out *KubeconfigStatus) { *out = *in diff --git a/charts/kamaji-crds/hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml b/charts/kamaji-crds/hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml new file mode 100644 index 0000000..2c7f72e --- /dev/null +++ b/charts/kamaji-crds/hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml @@ -0,0 +1,214 @@ +group: kamaji.clastix.io +names: + categories: + - kamaji + kind: KubeconfigGenerator + listKind: KubeconfigGeneratorList + plural: kubeconfiggenerators + shortNames: + - kc + singular: kubeconfiggenerator +scope: Cluster +versions: + - additionalPrinterColumns: + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + controlPlaneEndpointFrom: + default: admin.svc + description: |- + ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator. + The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`. + type: string + groups: + description: |- + Groups is resolved a set of strings used to assign the x509 organisations field. + It will be recognised by Kubernetes as user groups. + items: + description: |- + CompoundValue allows defining a static, or a dynamic value. + Options are mutually exclusive, just one should be picked up. + properties: + fromDefinition: + description: |- + FromDefinition is used to generate a dynamic value, + it uses the dot notation to access fields from the referenced TenantControlPlane object: + e.g.: metadata.name + type: string + stringValue: + description: StringValue is a static string value. + type: string + type: object + x-kubernetes-validations: + - message: Either stringValue or fromDefinition must be set, but not both. + rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition)) + type: array + namespaceSelector: + description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + tenantControlPlaneSelector: + description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + user: + description: User resolves to a string to identify the client, assigned to the x509 Common Name field. + properties: + fromDefinition: + description: |- + FromDefinition is used to generate a dynamic value, + it uses the dot notation to access fields from the referenced TenantControlPlane object: + e.g.: metadata.name + type: string + stringValue: + description: StringValue is a static string value. + type: string + type: object + x-kubernetes-validations: + - message: Either stringValue or fromDefinition must be set, but not both. + rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition)) + required: + - user + type: object + status: + description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator. + properties: + availableResources: + default: 0 + description: |- + AvailableResources is the sum of successfully generated resources. + In case of a different value compared to Resources, check the field errors. + type: integer + errors: + description: Errors is the list of failed kubeconfig generations. + items: + properties: + message: + description: Message is the error message recorded upon the last generator run. + type: string + resource: + description: Resource is the Namespaced name of the errored resource. + type: string + required: + - message + - resource + type: object + type: array + resources: + default: 0 + description: Resources is the sum of targeted TenantControlPlane objects. + type: integer + required: + - availableResources + - resources + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/kamaji-crds/templates/kamaji.clastix.io_kubeconfiggenerators.yaml b/charts/kamaji-crds/templates/kamaji.clastix.io_kubeconfiggenerators.yaml new file mode 100644 index 0000000..dd9b96f --- /dev/null +++ b/charts/kamaji-crds/templates/kamaji.clastix.io_kubeconfiggenerators.yaml @@ -0,0 +1,10 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }} + labels: + {{- include "kamaji-crds.labels" . | nindent 4 }} + name: kubeconfiggenerators.kamaji.clastix.io +spec: + {{ tpl (.Files.Get "hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml") . | nindent 2 }} diff --git a/charts/kamaji/README.md b/charts/kamaji/README.md index a1ba712..69a9cef 100644 --- a/charts/kamaji/README.md +++ b/charts/kamaji/README.md @@ -83,6 +83,24 @@ Here the values you can override: | image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | | | kamaji-etcd | object | `{"clusterDomain":"cluster.local","datastore":{"enabled":true,"name":"default"},"deploy":true,"fullnameOverride":"kamaji-etcd"}` | Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml | +| kubeconfigGenerator.affinity | object | `{}` | Kubernetes affinity rules to apply to Kubeconfig Generator controller pods | +| kubeconfigGenerator.enableLeaderElect | bool | `true` | Enables the leader election. | +| kubeconfigGenerator.enabled | bool | `false` | Toggle to deploy the Kubeconfig Generator Deployment. | +| kubeconfigGenerator.extraArgs | list | `[]` | A list of extra arguments to add to the Kubeconfig Generator controller default ones. | +| kubeconfigGenerator.fullnameOverride | string | `""` | | +| kubeconfigGenerator.healthProbeBindAddress | string | `":8081"` | The address the probe endpoint binds to. | +| kubeconfigGenerator.loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) | +| kubeconfigGenerator.nodeSelector | object | `{}` | Kubernetes node selector rules to schedule Kubeconfig Generator controller | +| kubeconfigGenerator.podAnnotations | object | `{}` | The annotations to apply to the Kubeconfig Generator controller pods. | +| kubeconfigGenerator.podSecurityContext | object | `{"runAsNonRoot":true}` | The securityContext to apply to the Kubeconfig Generator controller pods. | +| kubeconfigGenerator.replicaCount | int | `2` | The number of the pod replicas for the Kubeconfig Generator controller. | +| kubeconfigGenerator.resources.limits.cpu | string | `"200m"` | | +| kubeconfigGenerator.resources.limits.memory | string | `"512Mi"` | | +| kubeconfigGenerator.resources.requests.cpu | string | `"200m"` | | +| kubeconfigGenerator.resources.requests.memory | string | `"512Mi"` | | +| kubeconfigGenerator.securityContext | object | `{"allowPrivilegeEscalation":false}` | The securityContext to apply to the Kubeconfig Generator controller container only. | +| kubeconfigGenerator.serviceAccountOverride | string | `""` | The name of the service account to use. If not set, the root Kamaji one will be used. | +| kubeconfigGenerator.tolerations | list | `[]` | Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate | | livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"healthcheck"},"initialDelaySeconds":15,"periodSeconds":20}` | The livenessProbe for the controller container | | loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false) | | metricsBindAddress | string | `":8080"` | The address the metric endpoint binds to. (default ":8080") | diff --git a/charts/kamaji/controller-gen/clusterrole.yaml b/charts/kamaji/controller-gen/clusterrole.yaml index 9353019..f68894e 100644 --- a/charts/kamaji/controller-gen/clusterrole.yaml +++ b/charts/kamaji/controller-gen/clusterrole.yaml @@ -1,3 +1,11 @@ +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch - apiGroups: - apps resources: @@ -51,6 +59,7 @@ - kamaji.clastix.io resources: - datastores/status + - kubeconfiggenerators/status - tenantcontrolplanes/status verbs: - get @@ -59,6 +68,18 @@ - apiGroups: - kamaji.clastix.io resources: + - kubeconfiggenerators + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - kamaji.clastix.io + resources: + - kubeconfiggenerators/finalizers - tenantcontrolplanes/finalizers verbs: - update diff --git a/charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml b/charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml new file mode 100644 index 0000000..3febd16 --- /dev/null +++ b/charts/kamaji/crds/kamaji.clastix.io_kubeconfiggenerators.yaml @@ -0,0 +1,222 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert + controller-gen.kubebuilder.io/version: v0.16.1 + name: kubeconfiggenerators.kamaji.clastix.io +spec: + group: kamaji.clastix.io + names: + categories: + - kamaji + kind: KubeconfigGenerator + listKind: KubeconfigGeneratorList + plural: kubeconfiggenerators + shortNames: + - kc + singular: kubeconfiggenerator + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + controlPlaneEndpointFrom: + default: admin.svc + description: |- + ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator. + The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`. + type: string + groups: + description: |- + Groups is resolved a set of strings used to assign the x509 organisations field. + It will be recognised by Kubernetes as user groups. + items: + description: |- + CompoundValue allows defining a static, or a dynamic value. + Options are mutually exclusive, just one should be picked up. + properties: + fromDefinition: + description: |- + FromDefinition is used to generate a dynamic value, + it uses the dot notation to access fields from the referenced TenantControlPlane object: + e.g.: metadata.name + type: string + stringValue: + description: StringValue is a static string value. + type: string + type: object + x-kubernetes-validations: + - message: Either stringValue or fromDefinition must be set, but not both. + rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition)) + type: array + namespaceSelector: + description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + tenantControlPlaneSelector: + description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + user: + description: User resolves to a string to identify the client, assigned to the x509 Common Name field. + properties: + fromDefinition: + description: |- + FromDefinition is used to generate a dynamic value, + it uses the dot notation to access fields from the referenced TenantControlPlane object: + e.g.: metadata.name + type: string + stringValue: + description: StringValue is a static string value. + type: string + type: object + x-kubernetes-validations: + - message: Either stringValue or fromDefinition must be set, but not both. + rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition)) + required: + - user + type: object + status: + description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator. + properties: + availableResources: + default: 0 + description: |- + AvailableResources is the sum of successfully generated resources. + In case of a different value compared to Resources, check the field errors. + type: integer + errors: + description: Errors is the list of failed kubeconfig generations. + items: + properties: + message: + description: Message is the error message recorded upon the last generator run. + type: string + resource: + description: Resource is the Namespaced name of the errored resource. + type: string + required: + - message + - resource + type: object + type: array + resources: + default: 0 + description: Resources is the sum of targeted TenantControlPlane objects. + type: integer + required: + - availableResources + - resources + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/kamaji/templates/_helpers.tpl b/charts/kamaji/templates/_helpers.tpl index f44ca63..f0bf1be 100644 --- a/charts/kamaji/templates/_helpers.tpl +++ b/charts/kamaji/templates/_helpers.tpl @@ -89,3 +89,15 @@ Create the name of the cert-manager Certificate {{- define "kamaji.certificateName" -}} {{- printf "%s-serving-cert" (include "kamaji.fullname" .) }} {{- end }} + + +{{/* +Kubeconfig Generator Deployment name. +*/}} +{{- define "kamaji.kubeconfigGeneratorName" -}} +{{- if .Values.kubeconfigGenerator.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name "kubeconfig-generator" | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} diff --git a/charts/kamaji/templates/kubeconfiggenerator-deployment.yaml b/charts/kamaji/templates/kubeconfiggenerator-deployment.yaml new file mode 100644 index 0000000..d7199d2 --- /dev/null +++ b/charts/kamaji/templates/kubeconfiggenerator-deployment.yaml @@ -0,0 +1,54 @@ +{{- if .Values.kubeconfigGenerator.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + {{- include "kamaji.labels" . | nindent 4 }} + name: {{ include "kamaji.kubeconfigGeneratorName" . }} + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ .Values.kubeconfigGenerator.replicaCount }} + selector: + matchLabels: + {{- include "kamaji.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.kubeconfigGenerator.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "kamaji.selectorLabels" . | nindent 8 }} + spec: + securityContext: + {{- toYaml .Values.kubeconfigGenerator.podSecurityContext | nindent 8 }} + serviceAccountName: {{ default .Values.kubeconfigGenerator.serviceAccountOverride (include "kamaji.serviceAccountName" .) }} + containers: + - args: + - kubeconfig-generator + - --health-probe-bind-address={{ .Values.kubeconfigGenerator.healthProbeBindAddress }} + - --leader-elect={{ .Values.kubeconfigGenerator.enableLeaderElect }} + {{- if .Values.kubeconfigGenerator.loggingDevel.enable }}- --zap-devel{{- end }} + {{- with .Values.kubeconfigGenerator.extraArgs }} + {{- toYaml . | nindent 10 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: controller + resources: + {{- toYaml .Values.kubeconfigGenerator.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.kubeconfigGenerator.securityContext | nindent 12 }} + {{- with .Values.kubeconfigGenerator.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.kubeconfigGenerator.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.kubeconfigGenerator.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/kamaji/values.yaml b/charts/kamaji/values.yaml index 3d7b445..0e99af1 100644 --- a/charts/kamaji/values.yaml +++ b/charts/kamaji/values.yaml @@ -111,4 +111,48 @@ kamaji-etcd: # -- Disable the analytics traces collection telemetry: disabled: false - + +kubeconfigGenerator: + # -- Toggle to deploy the Kubeconfig Generator Deployment. + enabled: false + fullnameOverride: "" + # -- The number of the pod replicas for the Kubeconfig Generator controller. + replicaCount: 2 + # -- The annotations to apply to the Kubeconfig Generator controller pods. + podAnnotations: {} + # -- The securityContext to apply to the Kubeconfig Generator controller pods. + podSecurityContext: + runAsNonRoot: true + # -- The name of the service account to use. If not set, the root Kamaji one will be used. + serviceAccountOverride: "" + # -- The address the probe endpoint binds to. + healthProbeBindAddress: ":8081" + # -- Enables the leader election. + enableLeaderElect: true + loggingDevel: + # -- Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) + enable: false + # -- A list of extra arguments to add to the Kubeconfig Generator controller default ones. + extraArgs: [] + resources: + limits: + cpu: 200m + memory: 512Mi + requests: + cpu: 200m + memory: 512Mi + # -- The securityContext to apply to the Kubeconfig Generator controller container only. + securityContext: + allowPrivilegeEscalation: false + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + # -- Kubernetes node selector rules to schedule Kubeconfig Generator controller + nodeSelector: {} + # -- Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate + tolerations: [] + # -- Kubernetes affinity rules to apply to Kubeconfig Generator controller pods + affinity: {} diff --git a/cmd/kubeconfig-generator/cmd.go b/cmd/kubeconfig-generator/cmd.go new file mode 100644 index 0000000..404d231 --- /dev/null +++ b/cmd/kubeconfig-generator/cmd.go @@ -0,0 +1,167 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package kubeconfiggenerator + +import ( + "flag" + "fmt" + "io" + "os" + goRuntime "runtime" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + "github.com/clastix/kamaji/controllers" + "github.com/clastix/kamaji/internal" +) + +func NewCmd(scheme *runtime.Scheme) *cobra.Command { + // CLI flags + var ( + metricsBindAddress string + healthProbeBindAddress string + leaderElect bool + controllerReconcileTimeout time.Duration + cacheResyncPeriod time.Duration + managerNamespace string + certificateExpirationDeadline time.Duration + ) + + cmd := &cobra.Command{ + Use: "kubeconfig-generator", + Short: "Start the Kubeconfig Generator manager", + SilenceErrors: false, + SilenceUsage: true, + PreRunE: func(*cobra.Command, []string) error { + // Avoid polluting stdout with useless details by the underlying klog implementations + klog.SetOutput(io.Discard) + klog.LogToStderr(false) + + if certificateExpirationDeadline < 24*time.Hour { + return fmt.Errorf("certificate expiration deadline must be at least 24 hours") + } + + return nil + }, + RunE: func(*cobra.Command, []string) error { + ctx := ctrl.SetupSignalHandler() + + setupLog := ctrl.Log.WithName("kubeconfig-generator") + + setupLog.Info(fmt.Sprintf("Kamaji version %s %s%s", internal.GitTag, internal.GitCommit, internal.GitDirty)) + setupLog.Info(fmt.Sprintf("Build from: %s", internal.GitRepo)) + setupLog.Info(fmt.Sprintf("Build date: %s", internal.BuildTime)) + setupLog.Info(fmt.Sprintf("Go Version: %s", goRuntime.Version())) + setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", goRuntime.GOOS, goRuntime.GOARCH)) + + ctrlOpts := ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsBindAddress, + }, + HealthProbeBindAddress: healthProbeBindAddress, + LeaderElection: leaderElect, + LeaderElectionNamespace: managerNamespace, + LeaderElectionID: "kubeconfiggenerator.kamaji.clastix.io", + NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) { + opts.SyncPeriod = &cacheResyncPeriod + + return cache.New(config, opts) + }, + } + + triggerChan := make(chan event.GenericEvent) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrlOpts) + if err != nil { + setupLog.Error(err, "unable to start manager") + + return err + } + + setupLog.Info("setting probes") + { + if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + + return err + } + if err = mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + + return err + } + } + + certController := &controllers.CertificateLifecycle{Channel: triggerChan, Deadline: certificateExpirationDeadline} + certController.EnqueueFn = certController.EnqueueForKubeconfigGenerator + if err = certController.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CertificateLifecycle") + + return err + } + + if err = (&controllers.KubeconfigGeneratorWatcher{ + Client: mgr.GetClient(), + GeneratorChan: triggerChan, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "KubeconfigGeneratorWatcher") + + return err + } + + if err = (&controllers.KubeconfigGeneratorReconciler{ + Client: mgr.GetClient(), + NotValidThreshold: certificateExpirationDeadline, + CertificateChan: triggerChan, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "KubeconfigGenerator") + + return err + } + + setupLog.Info("starting manager") + if err = mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + + return err + } + + return nil + }, + } + // Setting zap logger + zapfs := flag.NewFlagSet("zap", flag.ExitOnError) + opts := zap.Options{ + Development: true, + } + opts.BindFlags(zapfs) + cmd.Flags().AddGoFlagSet(zapfs) + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // Setting CLI flags + cmd.Flags().StringVar(&metricsBindAddress, "metrics-bind-address", ":8090", "The address the metric endpoint binds to.") + cmd.Flags().StringVar(&healthProbeBindAddress, "health-probe-bind-address", ":8091", "The address the probe endpoint binds to.") + cmd.Flags().BoolVar(&leaderElect, "leader-elect", true, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + cmd.Flags().DurationVar(&controllerReconcileTimeout, "controller-reconcile-timeout", 30*time.Second, "The reconciliation request timeout before the controller withdraw the external resource calls, such as dealing with the Datastore, or the Tenant Control Plane API endpoint.") + cmd.Flags().DurationVar(&cacheResyncPeriod, "cache-resync-period", 10*time.Hour, "The controller-runtime.Manager cache resync period.") + cmd.Flags().StringVar(&managerNamespace, "pod-namespace", os.Getenv("POD_NAMESPACE"), "The Kubernetes Namespace on which the Operator is running in, required for the TenantControlPlane migration jobs.") + cmd.Flags().DurationVar(&certificateExpirationDeadline, "certificate-expiration-deadline", 24*time.Hour, "Define the deadline upon certificate expiration to start the renewal process, cannot be less than a 24 hours.") + + cobra.OnInitialize(func() { + viper.AutomaticEnv() + }) + + return cmd +} diff --git a/cmd/manager/cmd.go b/cmd/manager/cmd.go index f2af7d8..82e17cc 100644 --- a/cmd/manager/cmd.go +++ b/cmd/manager/cmd.go @@ -4,6 +4,7 @@ package manager import ( + "context" "flag" "fmt" "io" @@ -62,8 +63,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command { webhookCAPath string ) - ctx := ctrl.SetupSignalHandler() - cmd := &cobra.Command{ Use: "manager", Short: "Start the Kamaji Kubernetes Operator", @@ -86,7 +85,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command { return fmt.Errorf("unable to read webhook CA: %w", err) } - if err = datastoreutils.CheckExists(ctx, scheme, datastore); err != nil { + if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil { return err } @@ -97,6 +96,8 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command { return nil }, RunE: func(*cobra.Command, []string) error { + ctx := ctrl.SetupSignalHandler() + setupLog := ctrl.Log.WithName("setup") setupLog.Info(fmt.Sprintf("Kamaji version %s %s%s", internal.GitTag, internal.GitCommit, internal.GitDirty)) @@ -193,7 +194,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command { } } - if err = (&controllers.CertificateLifecycle{Channel: certChannel, Deadline: certificateExpirationDeadline}).SetupWithManager(mgr); err != nil { + certController := &controllers.CertificateLifecycle{Channel: certChannel, Deadline: certificateExpirationDeadline} + certController.EnqueueFn = certController.EnqueueForTenantControlPlane + + if err = certController.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CertificateLifecycle") return err diff --git a/config/samples/kamaji_v1alpha1_kubeconfiggenerator.yaml b/config/samples/kamaji_v1alpha1_kubeconfiggenerator.yaml new file mode 100644 index 0000000..ed42174 --- /dev/null +++ b/config/samples/kamaji_v1alpha1_kubeconfiggenerator.yaml @@ -0,0 +1,21 @@ +apiVersion: kamaji.clastix.io/v1alpha1 +kind: KubeconfigGenerator +metadata: + name: tenant +spec: + controlPlaneEndpointFrom: admin.conf + groups: + - stringValue: custom.group.tld + - fromDefinition: metadata.namespace + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: Exists + values: [] + tenantControlPlaneSelector: + matchExpressions: + - key: tenant.clastix.io + operator: Exists + values: [] + user: + fromDefinition: metadata.name diff --git a/controllers/certificate_lifecycle_controller.go b/controllers/certificate_lifecycle_controller.go index 00f8c9d..6530abe 100644 --- a/controllers/certificate_lifecycle_controller.go +++ b/controllers/certificate_lifecycle_controller.go @@ -31,8 +31,9 @@ import ( ) type CertificateLifecycle struct { - Channel chan event.GenericEvent - Deadline time.Duration + Channel chan event.GenericEvent + Deadline time.Duration + EnqueueFn func(secret *corev1.Secret) client client.Client } @@ -91,12 +92,7 @@ func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile. if deadline.After(crt.NotAfter) { logger.Info("certificate near expiration, must be rotated") - s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{ - ObjectMeta: metav1.ObjectMeta{ - Name: secret.GetOwnerReferences()[0].Name, - Namespace: secret.Namespace, - }, - }} + s.EnqueueFn(&secret) logger.Info("certificate rotation triggered") @@ -110,6 +106,35 @@ func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile. return reconcile.Result{RequeueAfter: after}, nil } +func (s *CertificateLifecycle) EnqueueForTenantControlPlane(secret *corev1.Secret) { + for _, or := range secret.GetOwnerReferences() { + if or.Kind != "TenantControlPlane" { + continue + } + + s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: or.Name, + Namespace: secret.Namespace, + }, + }} + } +} + +func (s *CertificateLifecycle) EnqueueForKubeconfigGenerator(secret *corev1.Secret) { + for _, or := range secret.GetOwnerReferences() { + if or.Kind != "KubeconfigGenerator" { + continue + } + + s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: or.Name, + }, + }} + } +} + func (s *CertificateLifecycle) extractCertificateFromBareSecret(secret corev1.Secret) (*x509.Certificate, error) { var crt *x509.Certificate var err error diff --git a/controllers/kubeconfiggenerator_controller.go b/controllers/kubeconfiggenerator_controller.go new file mode 100644 index 0000000..0824b13 --- /dev/null +++ b/controllers/kubeconfiggenerator_controller.go @@ -0,0 +1,444 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package controllers + +import ( + "bytes" + "context" + "crypto/x509" + "fmt" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1" + certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + "k8s.io/client-go/util/workqueue" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/util" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" + "github.com/clastix/kamaji/controllers/utils" + "github.com/clastix/kamaji/internal/constants" + "github.com/clastix/kamaji/internal/crypto" + "github.com/clastix/kamaji/internal/resources" + "github.com/clastix/kamaji/internal/utilities" +) + +type KubeconfigGeneratorReconciler struct { + Client client.Client + NotValidThreshold time.Duration + CertificateChan chan event.GenericEvent +} + +//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch +//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators,verbs=get;list;watch;create;update;patch +//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kamaji.clastix.io,resources=kubeconfiggenerators/finalizers,verbs=update + +func (r *KubeconfigGeneratorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("reconciling resource") + + var generator kamajiv1alpha1.KubeconfigGenerator + if err := r.Client.Get(ctx, req.NamespacedName, &generator); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("resource may have been deleted, skipping") + + return ctrl.Result{}, nil + } + + logger.Error(err, "cannot retrieve the required resource") + + return ctrl.Result{}, err + } + + if utils.IsPaused(&generator) { + logger.Info("paused reconciliation, no further actions") + + return ctrl.Result{}, nil + } + + status, err := r.handle(ctx, &generator) + if err != nil { + logger.Error(err, "cannot handle the request") + + return ctrl.Result{}, err + } + + generator.Status = status + + if statusErr := r.Client.Status().Update(ctx, &generator); statusErr != nil { + logger.Error(statusErr, "cannot update resource status") + + return ctrl.Result{}, statusErr + } + + logger.Info("reconciling completed") + + return ctrl.Result{}, nil +} + +func (r *KubeconfigGeneratorReconciler) handle(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator) (kamajiv1alpha1.KubeconfigGeneratorStatus, error) { + nsSelector, nsErr := metav1.LabelSelectorAsSelector(&generator.Spec.NamespaceSelector) + if nsErr != nil { + return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(nsErr, "NamespaceSelector contains an error") + } + + var namespaceList corev1.NamespaceList + if err := r.Client.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: nsSelector}); err != nil { + return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter Namespace objects using provided selector") + } + + var targets []kamajiv1alpha1.TenantControlPlane + + for _, ns := range namespaceList.Items { + tcpSelector, tcpErr := metav1.LabelSelectorAsSelector(&generator.Spec.TenantControlPlaneSelector) + if tcpErr != nil { + return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(tcpErr, "TenantControlPlaneSelector contains an error") + } + + var tcpList kamajiv1alpha1.TenantControlPlaneList + if err := r.Client.List(ctx, &tcpList, &client.ListOptions{Namespace: ns.GetName(), LabelSelector: tcpSelector}); err != nil { + return kamajiv1alpha1.KubeconfigGeneratorStatus{}, errors.Wrap(err, "cannot filter TenantControlPlane objects using provided selector") + } + + targets = append(targets, tcpList.Items...) + } + + sort.Slice(targets, func(i, j int) bool { + return client.ObjectKeyFromObject(&targets[i]).String() < client.ObjectKeyFromObject(&targets[j]).String() + }) + + status := kamajiv1alpha1.KubeconfigGeneratorStatus{ + Resources: len(targets), + AvailableResources: len(targets), + } + + for _, tcp := range targets { + if err := r.process(ctx, generator, tcp); err != nil { + status.Errors = append(status.Errors, *err) + status.AvailableResources-- + } + } + + return status, nil +} + +func (r *KubeconfigGeneratorReconciler) process(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator, tcp kamajiv1alpha1.TenantControlPlane) *kamajiv1alpha1.KubeconfigGeneratorStatusError { + statusErr := kamajiv1alpha1.KubeconfigGeneratorStatusError{ + Resource: client.ObjectKeyFromObject(&tcp).String(), + } + + var adminSecret corev1.Secret + + if tcp.Status.KubeConfig.Admin.SecretName == "" { + statusErr.Message = "the admin kubeconfig is not yet generated" + + return &statusErr + } + + if err := r.Client.Get(ctx, types.NamespacedName{Name: tcp.Status.KubeConfig.Admin.SecretName, Namespace: tcp.GetNamespace()}, &adminSecret); err != nil { + statusErr.Message = fmt.Sprintf("an error occurred retrieving the admin Kubeconfig: %s", err.Error()) + + return &statusErr + } + + kubeconfigTmpl, kcErr := utilities.DecodeKubeconfig(adminSecret, generator.Spec.ControlPlaneEndpointFrom) + if kcErr != nil { + statusErr.Message = fmt.Sprintf("unable to decode Kubeconfig template: %s", kcErr.Error()) + + return &statusErr + } + + uMap, uErr := runtime.DefaultUnstructuredConverter.ToUnstructured(&tcp) + if uErr != nil { + statusErr.Message = fmt.Sprintf("cannot convert the resource to a map: %s", uErr) + + return &statusErr + } + + var user string + groups := sets.New[string]() + + for _, group := range generator.Spec.Groups { + switch { + case group.StringValue != "": + groups.Insert(group.StringValue) + case group.FromDefinition != "": + v, ok, vErr := unstructured.NestedString(uMap, strings.Split(group.FromDefinition, ".")...) + switch { + case vErr != nil: + statusErr.Message = fmt.Sprintf("cannot run NestedString %q due to an error: %s", group.FromDefinition, vErr.Error()) + + return &statusErr + case !ok: + statusErr.Message = fmt.Sprintf("provided dot notation %q is not found", group.FromDefinition) + + return &statusErr + default: + groups.Insert(v) + } + default: + statusErr.Message = "at least a StringValue or FromDefinition Group value must be provided" + + return &statusErr + } + } + + switch { + case generator.Spec.User.StringValue != "": + user = generator.Spec.User.StringValue + case generator.Spec.User.FromDefinition != "": + v, ok, vErr := unstructured.NestedString(uMap, strings.Split(generator.Spec.User.FromDefinition, ".")...) + + switch { + case vErr != nil: + statusErr.Message = fmt.Sprintf("cannot run NestedString %q due to an error: %s", generator.Spec.User.FromDefinition, vErr.Error()) + + return &statusErr + case !ok: + statusErr.Message = fmt.Sprintf("provided dot notation %q is not found", generator.Spec.User.FromDefinition) + + return &statusErr + default: + user = v + } + default: + statusErr.Message = "at least a StringValue or FromDefinition for the user field must be provided" + + return &statusErr + } + + var resultSecret corev1.Secret + resultSecret.SetName(tcp.Name + "-" + generator.Name) + resultSecret.SetNamespace(tcp.Namespace) + + objectKey := client.ObjectKeyFromObject(&resultSecret) + + if err := r.Client.Get(ctx, objectKey, &resultSecret); err != nil { + if !apierrors.IsNotFound(err) { + statusErr.Message = fmt.Sprintf("the secret %q cannot be generated", objectKey.String()) + + return &statusErr + } + + if generateErr := r.generate(ctx, generator, &resultSecret, kubeconfigTmpl, &tcp, groups, user); generateErr != nil { + statusErr.Message = fmt.Sprintf("an error occurred generating the %q Secret: %s", objectKey.String(), generateErr.Error()) + + return &statusErr + } + + return nil + } + + isValid, validateErr := r.isValid(&resultSecret, kubeconfigTmpl, groups, user) + switch { + case !isValid: + if generateErr := r.generate(ctx, generator, &resultSecret, kubeconfigTmpl, &tcp, groups, user); generateErr != nil { + statusErr.Message = fmt.Sprintf("an error occurred regenerating the %q Secret: %s", objectKey.String(), generateErr.Error()) + + return &statusErr + } + + return nil + case validateErr != nil: + statusErr.Message = fmt.Sprintf("an error occurred checking validation for %q Secret: %s", objectKey.String(), validateErr.Error()) + + return &statusErr + default: + return nil + } +} + +func (r *KubeconfigGeneratorReconciler) generate(ctx context.Context, generator *kamajiv1alpha1.KubeconfigGenerator, secret *corev1.Secret, tmpl *clientcmdapiv1.Config, tcp *kamajiv1alpha1.TenantControlPlane, groups sets.Set[string], user string) error { + _, config, err := resources.GetKubeadmManifestDeps(ctx, r.Client, tcp) + if err != nil { + return err + } + + clientCertConfig := pkiutil.CertConfig{ + Config: certutil.Config{ + CommonName: user, + Organization: groups.UnsortedList(), + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, + NotAfter: util.StartTimeUTC().Add(kubeadmconstants.CertificateValidityPeriod), + EncryptionAlgorithm: config.InitConfiguration.ClusterConfiguration.EncryptionAlgorithmType(), + } + + var caSecret corev1.Secret + if caErr := r.Client.Get(ctx, types.NamespacedName{Namespace: tcp.Namespace, Name: tcp.Status.Certificates.CA.SecretName}, &caSecret); caErr != nil { + return errors.Wrap(caErr, "cannot retrieve Certificate Authority") + } + + caCert, crtErr := crypto.ParseCertificateBytes(caSecret.Data[kubeadmconstants.CACertName]) + if crtErr != nil { + return errors.Wrap(crtErr, "cannot parse Certificate Authority certificate") + } + + caKey, keyErr := crypto.ParsePrivateKeyBytes(caSecret.Data[kubeadmconstants.CAKeyName]) + if keyErr != nil { + return errors.Wrap(keyErr, "cannot parse Certificate Authority key") + } + + clientCert, clientKey, err := pkiutil.NewCertAndKey(caCert, caKey, &clientCertConfig) + + contextUserName := generator.Name + + for name := range tmpl.AuthInfos { + tmpl.AuthInfos[name].Name = contextUserName + tmpl.AuthInfos[name].AuthInfo.ClientCertificateData = pkiutil.EncodeCertPEM(clientCert) + tmpl.AuthInfos[name].AuthInfo.ClientKeyData, err = keyutil.MarshalPrivateKeyToPEM(clientKey) + if err != nil { + return errors.Wrap(err, "cannot marshal private key to PEM") + } + } + + for name := range tmpl.Contexts { + tmpl.Contexts[name].Name = contextUserName + tmpl.Contexts[name].Context.AuthInfo = contextUserName + } + + tmpl.CurrentContext = contextUserName + + _, err = utilities.CreateOrUpdateWithConflict(ctx, r.Client, secret, func() error { + labels := secret.GetLabels() + if labels == nil { + labels = map[string]string{} + } + + labels[kamajiv1alpha1.ManagedByLabel] = generator.Name + labels[kamajiv1alpha1.ManagedForLabel] = tcp.Name + labels[constants.ControllerLabelResource] = utilities.CertificateKubeconfigLabel + + secret.SetLabels(labels) + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + + secret.Data["value"], err = utilities.EncodeToYaml(tmpl) + if err != nil { + return errors.Wrap(err, "cannot encode generated Kubeconfig to YAML") + } + + if utilities.IsRotationRequested(secret) { + utilities.SetLastRotationTimestamp(secret) + } + + if orErr := controllerutil.SetOwnerReference(tcp, secret, r.Client.Scheme()); orErr != nil { + return orErr + } + + return ctrl.SetControllerReference(tcp, secret, r.Client.Scheme()) + }) + if err != nil { + return errors.Wrap(err, "cannot create or update generated Kubeconfig") + } + + return nil +} + +func (r *KubeconfigGeneratorReconciler) isValid(secret *corev1.Secret, tmpl *clientcmdapiv1.Config, groups sets.Set[string], user string) (bool, error) { + if utilities.IsRotationRequested(secret) { + return false, nil + } + + concrete, decodeErr := utilities.DecodeKubeconfig(*secret, "value") + if decodeErr != nil { + return false, decodeErr + } + // Checking Certificate Authority validity + switch { + case len(concrete.Clusters) != len(tmpl.Clusters): + return false, nil + default: + for i := range tmpl.Clusters { + if !bytes.Equal(tmpl.Clusters[i].Cluster.CertificateAuthorityData, concrete.Clusters[i].Cluster.CertificateAuthorityData) { + return false, nil + } + + if tmpl.Clusters[i].Cluster.Server != concrete.Clusters[i].Cluster.Server { + return false, nil + } + } + } + + for _, auth := range concrete.AuthInfos { + valid, vErr := crypto.IsValidCertificateKeyPairBytes(auth.AuthInfo.ClientCertificateData, auth.AuthInfo.ClientKeyData, r.NotValidThreshold) + if vErr != nil { + return false, vErr + } + if !valid { + return false, nil + } + + crt, crtErr := crypto.ParseCertificateBytes(auth.AuthInfo.ClientCertificateData) + if crtErr != nil { + return false, crtErr + } + + if crt.Subject.CommonName != user { + return false, nil + } + + if !sets.New[string](crt.Subject.Organization...).Equal(groups) { + return false, nil + } + } + + return true, nil +} + +func (r *KubeconfigGeneratorReconciler) SetupWithManager(mgr manager.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kamajiv1alpha1.KubeconfigGenerator{}). + WatchesRawSource(source.Channel(r.CertificateChan, handler.Funcs{GenericFunc: func(_ context.Context, genericEvent event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) { + w.AddRateLimited(ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: genericEvent.Object.GetName(), + }, + }) + }})). + Watches(&corev1.Secret{}, handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []ctrl.Request { + if object.GetLabels() == nil { + return nil + } + + v, found := object.GetLabels()[kamajiv1alpha1.ManagedByLabel] + if !found { + return nil + } + + return []ctrl.Request{ + { + NamespacedName: types.NamespacedName{ + Name: v, + }, + }, + } + })). + Complete(r) +} diff --git a/controllers/kubeconfiggenerator_watcher.go b/controllers/kubeconfiggenerator_watcher.go new file mode 100644 index 0000000..b25ec6b --- /dev/null +++ b/controllers/kubeconfiggenerator_watcher.go @@ -0,0 +1,75 @@ +// Copyright 2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package controllers + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + + kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" +) + +type KubeconfigGeneratorWatcher struct { + Client client.Client + GeneratorChan chan event.GenericEvent +} + +func (r *KubeconfigGeneratorWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + logger.Info("reconciling resource") + + var tcp kamajiv1alpha1.TenantControlPlane + if err := r.Client.Get(ctx, req.NamespacedName, &tcp); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("resource may have been deleted, skipping") + + return ctrl.Result{}, nil + } + + logger.Error(err, "cannot retrieve the required resource") + + return ctrl.Result{}, err + } + + var generators kamajiv1alpha1.KubeconfigGeneratorList + if err := r.Client.List(ctx, &generators); err != nil { + logger.Error(err, "cannot list generators") + + return ctrl.Result{}, err + } + + for _, generator := range generators.Items { + sel, err := metav1.LabelSelectorAsSelector(&generator.Spec.TenantControlPlaneSelector) + if err != nil { + logger.Error(err, "cannot validate Selector", "generator", generator.Name) + + return ctrl.Result{}, err + } + + if sel.Matches(labels.Set(tcp.Labels)) { + logger.Info("pushing Generator", "generator", generator.Name) + + r.GeneratorChan <- event.GenericEvent{ + Object: &generator, + } + } + } + + return ctrl.Result{}, nil +} + +func (r *KubeconfigGeneratorWatcher) SetupWithManager(mgr manager.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kamajiv1alpha1.TenantControlPlane{}). + Complete(r) +} diff --git a/docs/content/guides/kubeconfig-generator.md b/docs/content/guides/kubeconfig-generator.md new file mode 100644 index 0000000..997995a --- /dev/null +++ b/docs/content/guides/kubeconfig-generator.md @@ -0,0 +1,114 @@ +# Kubeconfig Generator + +The **Kubeconfig Generator** is a Kamaji extension that simplifies the distribution of Kubeconfig files for tenant clusters managed through Kamaji. + +Instead of manually exporting and editing credentials, the generator automates the creation of kubeconfigs aligned with your organizational policies. + +## Motivation + +When managing multiple Tenant Control Planes (TCPs), cluster administrators often face two challenges: + +1. **Consistency**: ensuring kubeconfigs are generated with the correct user identity, groups, and endpoints +2. **Scalability**: distributing kubeconfigs to users across potentially dozens of tenant clusters without manual steps + +The `KubeconfigGenerator` resource addresses these problems by: + +- Selecting which TCPs to target via label selectors. +- Defining how to build user and group identities in kubeconfigs. +- Automatically maintaining kubeconfigs as new tenant clusters are created or updated. + +This provides a single, declarative way to manage kubeconfig lifecycle across all your tenants, +especially convenient if those cases where an Identity Provider can't be used to delegate access to Tenant Control Plane clusters. + +## How it Works + +### Selection + +- `namespaceSelector` filters the namespaces from which Tenant Control Planes are discovered. +- `tenantControlPlaneSelector` further refines which TCPs to include. + +### Identity Definition + +The `user` and `groups` fields use compound values, which can be either: + +- A static string (e.g., `developer`) +- A dynamic reference resolved from the TCP object (e.g., `metadata.name`) + +This allows kubeconfigs to be tailored to the cluster’s context or a fixed organizational pattern. + +### Endpoint Resolution + +The generator pulls the API server endpoint from the TCP’s `admin` kubeconfig. + +By default it uses the `admin.svc` template, but this can be overridden with the `controlPlaneEndpointFrom` field. + +### Status and Errors + +The resource keeps track of how many kubeconfigs were attempted, how many succeeded, +and provides detailed error reports for failed generations. + +## Typical Use Cases + +- **Platform Operators**: automatically distribute kubeconfigs to developers as new tenant clusters are provisioned. +- **Multi-team Environments**: ensure each team gets kubeconfigs with the correct groups for RBAC authorization. +- **Least Privilege Principle**: avoid distributing `cluster-admin` credentials with a fine-grained RBAC +- **Dynamic Access**: use `fromDefinition` references to bind kubeconfig identities directly to tenant metadata + (e.g., prefixing users with the TCP's name). + +## Example Scenario + +A SaaS provider runs multiple Tenant Control Planes, each corresponding to a different customer. +Instead of manually managing kubeconfigs for every customer environment, the operator defines a single `KubeconfigGenerator`: + +```yaml +apiVersion: kamaji.clastix.io/v1alpha1 +kind: KubeconfigGenerator +metadata: + name: tenant +spec: + # Select only Tenant Control Planes living in namespaces + # labeled as production environments + namespaceSelector: + matchLabels: + environment: production + # Match all Tenant Control Planes in those namespaces + tenantControlPlaneSelector: {} + # Assign a static group "customer-admins" + groups: + - stringValue: "customer-admins" + # Derive the user identity dynamically from the TenantControlPlane metadata + user: + fromDefinition: "metadata.name" + # Use the public admin endpoint from the TCP’s kubeconfig + controlPlaneEndpointFrom: "admin.conf" +``` + +- Matches all TCPs in namespaces labeled `environment=production`. +- Generates kubeconfigs with group `customer-admins`. +- Derives the user identity from the TCP’s `metadata.name`. + +As new tenants are created, their kubeconfigs are generated automatically and kept up to date. + +``` +$: kubectl get secret --all-namespaces -l kamaji.clastix.io/managed-by=tenant +NAMESPACE NAME TYPE DATA AGE +alpha-tnt env-133-tenant Opaque 1 12h +alpha-tnt env-130-tenant Opaque 1 2d +bravo-tnt prod-tenant Opaque 1 2h +charlie-tnt stable-tenant Opaque 1 1d +``` + +## Observability + +The generator exposes its status directly in the CRD: +- `resources`: total number of TCPs targeted. +- `availableResources`: successfully generated kubeconfigs. +- `errors`: list of failed kubeconfig generations, including the affected resource and error message. + +This allows quick debugging and operational awareness. + +## Deployment + +The _Kubeconfig Generator_ is **not** enabled by default since it's still in experimental state. + +It can be enabled using the Helm value `kubeconfigGenerator.enabled=true` which is defaulted to `false`. diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index eeb62bd..030f78d 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -27271,6 +27271,8 @@ Resource Types: - [DataStore](#datastore) +- [KubeconfigGenerator](#kubeconfiggenerator) + - [TenantControlPlane](#tenantcontrolplane) @@ -27985,6 +27987,415 @@ DataStoreStatus defines the observed state of DataStore. +### KubeconfigGenerator + + + + + +KubeconfigGenerator is the Schema for the kubeconfiggenerators API. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringkamaji.clastix.io/v1alpha1true
kindstringKubeconfigGeneratortrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject +
+
false
statusobject + KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
+
false
+ + +`KubeconfigGenerator.spec` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
userobject + User resolves to a string to identify the client, assigned to the x509 Common Name field.
+
true
controlPlaneEndpointFromstring + ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator. +The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
+
+ Default: admin.svc
+
false
groups[]object + Groups is resolved a set of strings used to assign the x509 organisations field. +It will be recognised by Kubernetes as user groups.
+
false
namespaceSelectorobject + NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
+
false
tenantControlPlaneSelectorobject + TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
+
false
+ + +`KubeconfigGenerator.spec.user` + + +User resolves to a string to identify the client, assigned to the x509 Common Name field. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
fromDefinitionstring + FromDefinition is used to generate a dynamic value, +it uses the dot notation to access fields from the referenced TenantControlPlane object: +e.g.: metadata.name
+
false
stringValuestring + StringValue is a static string value.
+
false
+ + +`KubeconfigGenerator.spec.groups[index]` + + +CompoundValue allows defining a static, or a dynamic value. +Options are mutually exclusive, just one should be picked up. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
fromDefinitionstring + FromDefinition is used to generate a dynamic value, +it uses the dot notation to access fields from the referenced TenantControlPlane object: +e.g.: metadata.name
+
false
stringValuestring + StringValue is a static string value.
+
false
+ + +`KubeconfigGenerator.spec.namespaceSelector` + + +NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
matchExpressions[]object + matchExpressions is a list of label selector requirements. The requirements are ANDed.
+
false
matchLabelsmap[string]string + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels +map is equivalent to an element of matchExpressions, whose key field is "key", the +operator is "In", and the values array contains only "value". The requirements are ANDed.
+
false
+ + +`KubeconfigGenerator.spec.namespaceSelector.matchExpressions[index]` + + +A label selector requirement is a selector that contains values, a key, and an operator that +relates the key and values. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + key is the label key that the selector applies to.
+
true
operatorstring + operator represents a key's relationship to a set of values. +Valid operators are In, NotIn, Exists and DoesNotExist.
+
true
values[]string + values is an array of string values. If the operator is In or NotIn, +the values array must be non-empty. If the operator is Exists or DoesNotExist, +the values array must be empty. This array is replaced during a strategic +merge patch.
+
false
+ + +`KubeconfigGenerator.spec.tenantControlPlaneSelector` + + +TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
matchExpressions[]object + matchExpressions is a list of label selector requirements. The requirements are ANDed.
+
false
matchLabelsmap[string]string + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels +map is equivalent to an element of matchExpressions, whose key field is "key", the +operator is "In", and the values array contains only "value". The requirements are ANDed.
+
false
+ + +`KubeconfigGenerator.spec.tenantControlPlaneSelector.matchExpressions[index]` + + +A label selector requirement is a selector that contains values, a key, and an operator that +relates the key and values. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + key is the label key that the selector applies to.
+
true
operatorstring + operator represents a key's relationship to a set of values. +Valid operators are In, NotIn, Exists and DoesNotExist.
+
true
values[]string + values is an array of string values. If the operator is In or NotIn, +the values array must be non-empty. If the operator is Exists or DoesNotExist, +the values array must be empty. This array is replaced during a strategic +merge patch.
+
false
+ + +`KubeconfigGenerator.status` + + +KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
availableResourcesinteger + AvailableResources is the sum of successfully generated resources. +In case of a different value compared to Resources, check the field errors.
+
+ Default: 0
+
true
resourcesinteger + Resources is the sum of targeted TenantControlPlane objects.
+
+ Default: 0
+
true
errors[]object + Errors is the list of failed kubeconfig generations.
+
false
+ + +`KubeconfigGenerator.status.errors[index]` + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
messagestring + Message is the error message recorded upon the last generator run.
+
true
resourcestring + Resource is the Namespaced name of the errored resource.
+
true
+ ### TenantControlPlane diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index c3c8bb1..05dc83c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -77,6 +77,7 @@ nav: - guides/datastore-migration.md - guides/gitops.md - guides/console.md + - guides/kubeconfig-generator.md - guides/upgrade.md - guides/monitoring.md - guides/terraform.md diff --git a/main.go b/main.go index dd14bfd..1e96855 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "github.com/clastix/kamaji/cmd" + kubeconfig_generator "github.com/clastix/kamaji/cmd/kubeconfig-generator" "github.com/clastix/kamaji/cmd/manager" "github.com/clastix/kamaji/cmd/migrate" ) @@ -16,9 +17,10 @@ import ( func main() { scheme := runtime.NewScheme() - root, mgr, migrator := cmd.NewCmd(scheme), manager.NewCmd(scheme), migrate.NewCmd(scheme) + root, mgr, migrator, kubeconfigGenerator := cmd.NewCmd(scheme), manager.NewCmd(scheme), migrate.NewCmd(scheme), kubeconfig_generator.NewCmd(scheme) root.AddCommand(mgr) root.AddCommand(migrator) + root.AddCommand(kubeconfigGenerator) if err := root.Execute(); err != nil { os.Exit(1)