From 46601c4186bcaa9aacdb8d57450e109fe48f080b Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Mon, 6 May 2024 15:08:53 -0500 Subject: [PATCH] Add machine user [ZITADOPER-1] --- build/flake.nix | 14 +- ops/chart/crds/machineuser-crd.yaml | 172 +++++++++++++++ ops/chart/templates/deployment.yaml | 7 +- ops/chart/values.yaml | 30 +-- src/PROJECT | 9 + src/api/v1alpha1/machineuser_types.go | 94 +++++++++ src/api/v1alpha1/zz_generated.deepcopy.go | 97 +++++++++ src/cmd/main.go | 4 + .../zitadel.topmanage.com_machineusers.yaml | 173 +++++++++++++++ src/config/crd/kustomization.yaml | 3 + .../patches/cainjection_in_machineusers.yaml | 7 + .../crd/patches/webhook_in_machineusers.yaml | 16 ++ src/config/rbac/machineuser_editor_role.yaml | 31 +++ src/config/rbac/machineuser_viewer_role.yaml | 27 +++ src/config/samples/kustomization.yaml | 1 + .../samples/zitadel_v1alpha1_machineuser.yaml | 12 ++ src/go.mod | 2 +- .../controller/machineuser_controller.go | 198 ++++++++++++++++++ .../machineuser_controller_finalizer.go | 90 ++++++++ 19 files changed, 965 insertions(+), 22 deletions(-) create mode 100644 ops/chart/crds/machineuser-crd.yaml create mode 100644 src/api/v1alpha1/machineuser_types.go create mode 100644 src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml create mode 100644 src/config/crd/patches/cainjection_in_machineusers.yaml create mode 100644 src/config/crd/patches/webhook_in_machineusers.yaml create mode 100644 src/config/rbac/machineuser_editor_role.yaml create mode 100644 src/config/rbac/machineuser_viewer_role.yaml create mode 100644 src/config/samples/zitadel_v1alpha1_machineuser.yaml create mode 100644 src/internal/controller/machineuser_controller.go create mode 100644 src/internal/controller/machineuser_controller_finalizer.go diff --git a/build/flake.nix b/build/flake.nix index af50ba9..aa7f4ae 100644 --- a/build/flake.nix +++ b/build/flake.nix @@ -16,8 +16,14 @@ version = "0.0.0"; src = ../src; vendorHash = "sha256-8zGXnliSEnac9ry3eITjsXFuKYwJvKAYXeZUB65/PPo="; - postInstallPhase = '' - cp cmd $out + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + dir="$GOPATH/bin" + [ -e "$dir" ] && cp -r $dir/cmd $out/manager + + runHook postInstall ''; }; dockerPackage = pkgs.dockerTools.buildImage { @@ -27,10 +33,10 @@ copyToRoot = pkgs.buildEnv { name = "operator"; paths = [ package ]; - pathsToLink = [ "/bin" ]; + pathsToLink = [ "/" ]; }; config = { - Cmd = [ "/bin/cmd" ]; + Cmd = [ "/manager" ]; WorkingDir = "/"; User = "65532:65532"; }; diff --git a/ops/chart/crds/machineuser-crd.yaml b/ops/chart/crds/machineuser-crd.yaml new file mode 100644 index 0000000..f32e665 --- /dev/null +++ b/ops/chart/crds/machineuser-crd.yaml @@ -0,0 +1,172 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: machineusers.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: MachineUser + listKind: MachineUserList + plural: machineusers + singular: machineuser + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MachineUser is the Schema for the machineusers 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: + description: MachineUserSpec defines the desired state of MachineUser + properties: + accessTokenType: + enum: + - ACCESS_TOKEN_TYPE_BEARER + - ACCESS_TOKEN_TYPE_JWT + type: string + zitadelClusterRef: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file' + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - accessTokenType + - zitadelClusterRef + type: object + status: + description: MachineUserStatus defines the observed state of MachineUser + properties: + conditions: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + keyId: + default: "" + type: string + userId: + default: "" + type: string + required: + - keyId + - userId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/ops/chart/templates/deployment.yaml b/ops/chart/templates/deployment.yaml index 142a8a8..b005ccd 100644 --- a/ops/chart/templates/deployment.yaml +++ b/ops/chart/templates/deployment.yaml @@ -54,7 +54,10 @@ spec: 10 }} securityContext: {{- toYaml .Values.controllerManager.kubeRbacProxy.containerSecurityContext | nindent 10 }} - - env: + - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + command: + - /manager + env: - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag @@ -79,4 +82,4 @@ spec: securityContext: runAsNonRoot: true serviceAccountName: {{ include "zitadel-k8s-operator.fullname" . }}-controller-manager - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/ops/chart/values.yaml b/ops/chart/values.yaml index 4170211..5fb6bf1 100644 --- a/ops/chart/values.yaml +++ b/ops/chart/values.yaml @@ -1,15 +1,15 @@ controllerManager: kubeRbacProxy: args: - - --secure-listen-address=0.0.0.0:8443 - - --upstream=http://127.0.0.1:8080/ - - --logtostderr=true - - --v=0 + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - - ALL + - ALL image: repository: gcr.io/kubebuilder/kube-rbac-proxy tag: v0.13.1 @@ -22,17 +22,17 @@ controllerManager: memory: 64Mi manager: args: - - --health-probe-bind-address=:8081 - - --metrics-bind-address=127.0.0.1:8080 - - --leader-elect + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - - ALL + - ALL image: - repository: - tag: + repository: controller + tag: latest resources: limits: cpu: 500m @@ -46,8 +46,8 @@ controllerManager: kubernetesClusterDomain: cluster.local metricsService: ports: - - name: https - port: 8443 - protocol: TCP - targetPort: https + - name: https + port: 8443 + protocol: TCP + targetPort: https type: ClusterIP diff --git a/src/PROJECT b/src/PROJECT index e477745..7b28a22 100644 --- a/src/PROJECT +++ b/src/PROJECT @@ -47,4 +47,13 @@ resources: kind: OIDCApp path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: topmanage.com + group: zitadel + kind: MachineUser + path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/src/api/v1alpha1/machineuser_types.go b/src/api/v1alpha1/machineuser_types.go new file mode 100644 index 0000000..f057ac9 --- /dev/null +++ b/src/api/v1alpha1/machineuser_types.go @@ -0,0 +1,94 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// MachineUserSpec defines the desired state of MachineUser +type MachineUserSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + ZitadelClusterRef ZitadelClusterRef `json:"zitadelClusterRef" webhook:"inmutable"` + // +kubebuilder:validation:Enum=ACCESS_TOKEN_TYPE_BEARER;ACCESS_TOKEN_TYPE_JWT + AccessTokenType string `json:"accessTokenType"` +} + +// MachineUserStatus defines the observed state of MachineUser +type MachineUserStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"} + Conditions []metav1.Condition `json:"conditions,omitempty"` + // +kubebuilder:default="" + UserId string `json:"userId"` + // +kubebuilder:default="" + KeyId string `json:"keyId"` +} + +func (d *MachineUserStatus) SetCondition(condition metav1.Condition) { + if d.Conditions == nil { + d.Conditions = make([]metav1.Condition, 0) + } + meta.SetStatusCondition(&d.Conditions, condition) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// MachineUser is the Schema for the machineusers API +type MachineUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MachineUserSpec `json:"spec,omitempty"` + Status MachineUserStatus `json:"status,omitempty"` +} + +func (d *MachineUser) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *MachineUser) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *MachineUser) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver) (*ZitadelClusterRef, error) { + return &d.Spec.ZitadelClusterRef, nil +} + +//+kubebuilder:object:root=true + +// MachineUserList contains a list of MachineUser +type MachineUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MachineUser `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MachineUser{}, &MachineUserList{}) +} diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 550223c..1efee6e 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -57,6 +57,103 @@ func (in *Image) DeepCopy() *Image { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineUser) DeepCopyInto(out *MachineUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUser. +func (in *MachineUser) DeepCopy() *MachineUser { + if in == nil { + return nil + } + out := new(MachineUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MachineUser) 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 *MachineUserList) DeepCopyInto(out *MachineUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MachineUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserList. +func (in *MachineUserList) DeepCopy() *MachineUserList { + if in == nil { + return nil + } + out := new(MachineUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MachineUserList) 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 *MachineUserSpec) DeepCopyInto(out *MachineUserSpec) { + *out = *in + out.ZitadelClusterRef = in.ZitadelClusterRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserSpec. +func (in *MachineUserSpec) DeepCopy() *MachineUserSpec { + if in == nil { + return nil + } + out := new(MachineUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineUserStatus) DeepCopyInto(out *MachineUserStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineUserStatus. +func (in *MachineUserStatus) DeepCopy() *MachineUserStatus { + if in == nil { + return nil + } + out := new(MachineUserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCApp) DeepCopyInto(out *OIDCApp) { *out = *in diff --git a/src/cmd/main.go b/src/cmd/main.go index 2347191..77f220e 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -129,6 +129,10 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "OIDCApp") os.Exit(1) } + if err = controller.NewMachineUserReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MachineUser") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml new file mode 100644 index 0000000..27fbee0 --- /dev/null +++ b/src/config/crd/bases/zitadel.topmanage.com_machineusers.yaml @@ -0,0 +1,173 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: machineusers.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: MachineUser + listKind: MachineUserList + plural: machineusers + singular: machineuser + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MachineUser is the Schema for the machineusers 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: + description: MachineUserSpec defines the desired state of MachineUser + properties: + accessTokenType: + enum: + - ACCESS_TOKEN_TYPE_BEARER + - ACCESS_TOKEN_TYPE_JWT + type: string + zitadelClusterRef: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file' + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - accessTokenType + - zitadelClusterRef + type: object + status: + description: MachineUserStatus defines the observed state of MachineUser + properties: + conditions: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + keyId: + default: "" + type: string + userId: + default: "" + type: string + required: + - keyId + - userId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/kustomization.yaml b/src/config/crd/kustomization.yaml index d661210..4d426aa 100644 --- a/src/config/crd/kustomization.yaml +++ b/src/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/zitadel.topmanage.com_organizations.yaml - bases/zitadel.topmanage.com_projects.yaml - bases/zitadel.topmanage.com_oidcapps.yaml +- bases/zitadel.topmanage.com_machineusers.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -15,6 +16,7 @@ patchesStrategicMerge: #- patches/webhook_in_organizations.yaml #- patches/webhook_in_projects.yaml #- patches/webhook_in_oidcapps.yaml +#- patches/webhook_in_machineusers.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -23,6 +25,7 @@ patchesStrategicMerge: #- patches/cainjection_in_organizations.yaml #- patches/cainjection_in_projects.yaml #- patches/cainjection_in_oidcapps.yaml +#- patches/cainjection_in_machineusers.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/src/config/crd/patches/cainjection_in_machineusers.yaml b/src/config/crd/patches/cainjection_in_machineusers.yaml new file mode 100644 index 0000000..49e3ea2 --- /dev/null +++ b/src/config/crd/patches/cainjection_in_machineusers.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: machineusers.zitadel.topmanage.com diff --git a/src/config/crd/patches/webhook_in_machineusers.yaml b/src/config/crd/patches/webhook_in_machineusers.yaml new file mode 100644 index 0000000..f4ca8cc --- /dev/null +++ b/src/config/crd/patches/webhook_in_machineusers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: machineusers.zitadel.topmanage.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/src/config/rbac/machineuser_editor_role.yaml b/src/config/rbac/machineuser_editor_role.yaml new file mode 100644 index 0000000..e38d5c7 --- /dev/null +++ b/src/config/rbac/machineuser_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit machineusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: machineuser-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: machineuser-editor-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - machineusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - machineusers/status + verbs: + - get diff --git a/src/config/rbac/machineuser_viewer_role.yaml b/src/config/rbac/machineuser_viewer_role.yaml new file mode 100644 index 0000000..970fdb4 --- /dev/null +++ b/src/config/rbac/machineuser_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view machineusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: machineuser-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: src + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + name: machineuser-viewer-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - machineusers + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - machineusers/status + verbs: + - get diff --git a/src/config/samples/kustomization.yaml b/src/config/samples/kustomization.yaml index e33d223..a9ca3f7 100644 --- a/src/config/samples/kustomization.yaml +++ b/src/config/samples/kustomization.yaml @@ -4,4 +4,5 @@ resources: - zitadel_v1alpha1_organization.yaml - zitadel_v1alpha1_project.yaml - zitadel_v1alpha1_oidcapp.yaml +- zitadel_v1alpha1_machineuser.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/src/config/samples/zitadel_v1alpha1_machineuser.yaml b/src/config/samples/zitadel_v1alpha1_machineuser.yaml new file mode 100644 index 0000000..5625cfe --- /dev/null +++ b/src/config/samples/zitadel_v1alpha1_machineuser.yaml @@ -0,0 +1,12 @@ +apiVersion: zitadel.topmanage.com/v1alpha1 +kind: MachineUser +metadata: + labels: + app.kubernetes.io/name: machineuser + app.kubernetes.io/instance: machineuser-sample + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: src + name: machineuser-sample +spec: + # TODO(user): Add fields here diff --git a/src/go.mod b/src/go.mod index 897cab5..e51cff5 100644 --- a/src/go.mod +++ b/src/go.mod @@ -15,6 +15,7 @@ require ( github.com/zitadel/zitadel-go/v2 v2.1.10 golang.org/x/oauth2 v0.18.0 google.golang.org/grpc v1.62.1 + google.golang.org/protobuf v1.33.0 gopkg.in/square/go-jose.v2 v2.6.0 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 @@ -93,7 +94,6 @@ require ( google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/src/internal/controller/machineuser_controller.go b/src/internal/controller/machineuser_controller.go new file mode 100644 index 0000000..cd818d7 --- /dev/null +++ b/src/internal/controller/machineuser_controller.go @@ -0,0 +1,198 @@ +package controller + +import ( + "context" + "fmt" + "strings" + "time" + + zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder" + condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel" + "github.com/zitadel/zitadel-go/v2/pkg/client/management" + "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn" + pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + user "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/user" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" +) + +// MachineUserReconciler reconciles a MachineUser object +type MachineUserReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *MachineUserReconciler { + return &MachineUserReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=oidcapps/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *MachineUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var MachineUser zitadelv1alpha1.MachineUser + if err := r.Get(ctx, req.NamespacedName, &MachineUser); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedMachineUserReconciler(r.Client, r.RefResolver, r.Builder, &MachineUser) + wf := newWrappedMachineUserFinalizer(r.Client, &MachineUser) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &MachineUser) + if err != nil { + return result, fmt.Errorf("error reconciling in MachineUserReconciler: %v", err) + } + return result, nil +} + +type wrappedMachineUserReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + MachineUser *zitadelv1alpha1.MachineUser + Builder *builder.Builder +} + +func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + MachineUser *zitadelv1alpha1.MachineUser) zitadel.WrappedReconciler { + return &wrappedMachineUserReconciler{ + Client: client, + refResolver: refResolver, + MachineUser: MachineUser, + Builder: builder, + } +} + +func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + // if wr.MachineUser.Status.AppId != "" { + // appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{ + // ProjectId: project.Status.ProjectId, + // AppId: string(wr.MachineUser.Status.AppId), + // }) + // if err != nil { + // return fmt.Errorf("Error getting MachineUser: %v", err) + // } + // if appResp.App != nil { + // _, err := ztdClient.UpdateMachineUserConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateMachineUserConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.MachineUser.Status.AppId, + // RedirectUris: wr.MachineUser.Spec.RedirectUris, + // ResponseTypes: responseTypes, + // GrantTypes: grantTypes, + // AppType: app.MachineUserType(app.MachineUserType_value[wr.MachineUser.Spec.AppType]), + // AuthMethodType: app.OIDCAuthMethodType(app.OIDCAuthMethodType_value[wr.MachineUser.Spec.AuthMethodType]), + // PostLogoutRedirectUris: wr.MachineUser.Spec.PostLogoutRedirectUris, + // DevMode: wr.MachineUser.Spec.DevMode, + // AccessTokenType: app.OIDCTokenType(app.OIDCTokenType_value[wr.MachineUser.Spec.AccessTokenType]), + // AccessTokenRoleAssertion: wr.MachineUser.Spec.AccessTokenRoleAssertion, + // IdTokenRoleAssertion: wr.MachineUser.Spec.IdTokenRoleAssertion, + // IdTokenUserinfoAssertion: wr.MachineUser.Spec.IdTokenUserinfoAssertion, + // ClockSkew: durationpb.New(wr.MachineUser.Spec.ClockSkew.Duration), + // AdditionalOrigins: wr.MachineUser.Spec.AdditionalOrigins, + // SkipNativeAppSuccessPage: wr.MachineUser.Spec.SkipNativeAppSuccessPage, + // }) + // if err != nil { + // if !strings.Contains(err.Error(), "No changes") { + // return fmt.Errorf("Error updating MachineUser: %v", err) + // } + // } + // return nil + // } + // } + zitadel, err := wr.refResolver.ZitadelCluster(ctx, &wr.MachineUser.Spec.ZitadelClusterRef, wr.MachineUser.Namespace) + if err != nil { + return err + } + + resp, err := ztdClient.AddMachineUser(ctx, + &pb.AddMachineUserRequest{ + Name: wr.MachineUser.Name, + UserName: wr.MachineUser.Name, + Description: wr.MachineUser.Name, + AccessTokenType: user.AccessTokenType(user.AccessTokenType_value[wr.MachineUser.Spec.AccessTokenType]), + }, + ) + if err != nil { + if strings.Contains(err.Error(), "AlreadyExists") { + return nil + } + return fmt.Errorf("error creating MachineUser in Zitadel: %v", err) + } + + patch := ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) + wr.MachineUser.Status.UserId = resp.UserId + if err := wr.Client.Status().Patch(ctx, wr.MachineUser, patch); err != nil { + return err + } + + if wr.MachineUser.Status.KeyId != "" { + respKey, err := ztdClient.AddMachineKey(ctx, &pb.AddMachineKeyRequest{ + UserId: resp.UserId, + Type: authn.KeyType_KEY_TYPE_JSON, + ExpirationDate: nil, + }) + if err != nil { + return fmt.Errorf("Error Adding MachineKey: %v", err) + } + key := types.NamespacedName{ + Name: wr.MachineUser.Name + "-key-secret", + Namespace: wr.MachineUser.Namespace, + } + secret, err := wr.Builder.BuildSecret(builder.SecretOpts{ + Zitadel: zitadel, + Key: key, + Immutable: true, + Data: map[string][]byte{ + "key": respKey.KeyDetails, + }, + }, wr.MachineUser) + if err != nil { + return fmt.Errorf("error building Secret: %v", err) + } + if err := wr.Create(ctx, secret); err != nil { + return fmt.Errorf("error creating machine key Secret: %v", err) + } + patch = ctrlClient.MergeFrom(wr.MachineUser.DeepCopy()) + wr.MachineUser.Status.KeyId = respKey.KeyId + return wr.Client.Status().Patch(ctx, wr.MachineUser, patch) + } + + return nil +} + +func (wr *wrappedMachineUserReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.MachineUser.DeepCopy()) + patcher(&wr.MachineUser.Status) + + if err := wr.Client.Status().Patch(ctx, wr.MachineUser, patch); err != nil { + return fmt.Errorf("error patching MachineUser status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MachineUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.MachineUser{}). + Owns(&corev1.Secret{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/machineuser_controller_finalizer.go b/src/internal/controller/machineuser_controller_finalizer.go new file mode 100644 index 0000000..8c1ab44 --- /dev/null +++ b/src/internal/controller/machineuser_controller_finalizer.go @@ -0,0 +1,90 @@ +package controller + +import ( + "strings" + + zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" + "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel" + + "context" + "fmt" + + "github.com/zitadel/zitadel-go/v2/pkg/client/management" + pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + machineuserFinalizerName = "machineuser.zitadel.topmanage.com/machineuser" +) + +type wrappedMachineUserFinalizer struct { + client.Client + machineuser *zitadelv1alpha1.MachineUser +} + +func newWrappedMachineUserFinalizer(client client.Client, machineuser *zitadelv1alpha1.MachineUser) zitadel.WrappedFinalizer { + return &wrappedMachineUserFinalizer{ + Client: client, + machineuser: machineuser, + } +} + +func (wf *wrappedMachineUserFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) { + controllerutil.AddFinalizer(machineuser, machineuserFinalizerName) + }) +} + +func (wf *wrappedMachineUserFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.machineuser, func(machineuser *zitadelv1alpha1.MachineUser) { + controllerutil.RemoveFinalizer(wf.machineuser, machineuserFinalizerName) + }) +} + +func (wr *wrappedMachineUserFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.machineuser, machineuserFinalizerName) +} + +func (wf *wrappedMachineUserFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + if wf.machineuser.Status.UserId == "" { + return nil + } + { + _, err := ztdClient.GetUserByID(ctx, &pb.GetUserByIDRequest{ + Id: wf.machineuser.Status.UserId, + }) + if err != nil { + if strings.Contains(err.Error(), `User doesn't exist`) { + return nil + } + return err + } + } + _, err := ztdClient.RemoveUser(ctx, &pb.RemoveUserRequest{ + Id: wf.machineuser.Status.UserId, + }) + if err != nil { + return err + } + return nil +} + +func (wr *wrappedMachineUserFinalizer) patch(ctx context.Context, machineuser *zitadelv1alpha1.MachineUser, + patchFn func(*zitadelv1alpha1.MachineUser)) error { + patch := ctrlClient.MergeFrom(machineuser.DeepCopy()) + patchFn(machineuser) + + if err := wr.Client.Patch(ctx, machineuser, patch); err != nil { + return fmt.Errorf("error patching MachineUser finalizer: %v", err) + } + return nil +}