From aa5a411251a8484870dbf736a8dbbb4b6ca0df9a Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Mon, 17 Jun 2024 15:46:05 -0500 Subject: [PATCH 1/5] Add Flows and Actions [ZITADOPER-4] --- ops/chart/crds/action-crd.yaml | 173 ++++++++++++++ ops/chart/crds/flow-crd.yaml | 221 +++++++++++++++++ ops/chart/templates/manager-rbac.yaml | 52 ++++ src/PROJECT | 18 ++ src/api/v1alpha1/action_types.go | 109 +++++++++ src/api/v1alpha1/flow_types.go | 106 +++++++++ src/api/v1alpha1/ref_types.go | 7 + src/api/v1alpha1/refresolver.go | 21 ++ src/api/v1alpha1/zz_generated.deepcopy.go | 220 +++++++++++++++++ src/cmd/main.go | 8 + .../bases/zitadel.topmanage.com_actions.yaml | 174 ++++++++++++++ .../bases/zitadel.topmanage.com_flows.yaml | 222 ++++++++++++++++++ src/config/crd/kustomization.yaml | 6 + .../crd/patches/cainjection_in_actions.yaml | 7 + .../crd/patches/cainjection_in_flows.yaml | 7 + .../crd/patches/webhook_in_actions.yaml | 16 ++ src/config/crd/patches/webhook_in_flows.yaml | 16 ++ src/config/rbac/action_editor_role.yaml | 31 +++ src/config/rbac/action_viewer_role.yaml | 27 +++ src/config/rbac/flow_editor_role.yaml | 31 +++ src/config/rbac/flow_viewer_role.yaml | 27 +++ src/config/rbac/role.yaml | 52 ++++ src/config/samples/kustomization.yaml | 2 + .../samples/zitadel_v1alpha1_action.yaml | 12 + src/config/samples/zitadel_v1alpha1_flow.yaml | 12 + src/internal/controller/action_controller.go | 191 +++++++++++++++ .../controller/action_controller_finalizer.go | 94 ++++++++ src/internal/controller/flow_controller.go | 164 +++++++++++++ .../controller/flow_controller_finalizer.go | 82 +++++++ 29 files changed, 2108 insertions(+) create mode 100644 ops/chart/crds/action-crd.yaml create mode 100644 ops/chart/crds/flow-crd.yaml create mode 100644 src/api/v1alpha1/action_types.go create mode 100644 src/api/v1alpha1/flow_types.go create mode 100644 src/config/crd/bases/zitadel.topmanage.com_actions.yaml create mode 100644 src/config/crd/bases/zitadel.topmanage.com_flows.yaml create mode 100644 src/config/crd/patches/cainjection_in_actions.yaml create mode 100644 src/config/crd/patches/cainjection_in_flows.yaml create mode 100644 src/config/crd/patches/webhook_in_actions.yaml create mode 100644 src/config/crd/patches/webhook_in_flows.yaml create mode 100644 src/config/rbac/action_editor_role.yaml create mode 100644 src/config/rbac/action_viewer_role.yaml create mode 100644 src/config/rbac/flow_editor_role.yaml create mode 100644 src/config/rbac/flow_viewer_role.yaml create mode 100644 src/config/samples/zitadel_v1alpha1_action.yaml create mode 100644 src/config/samples/zitadel_v1alpha1_flow.yaml create mode 100644 src/internal/controller/action_controller.go create mode 100644 src/internal/controller/action_controller_finalizer.go create mode 100644 src/internal/controller/flow_controller.go create mode 100644 src/internal/controller/flow_controller_finalizer.go diff --git a/ops/chart/crds/action-crd.yaml b/ops/chart/crds/action-crd.yaml new file mode 100644 index 0000000..6bc560a --- /dev/null +++ b/ops/chart/crds/action-crd.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: actions.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: Action + listKind: ActionList + plural: actions + singular: action + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Action is the Schema for the actions 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: ActionSpec defines the desired state of Action + properties: + allowedToFail: + default: true + type: boolean + organizationRef: + 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 + script: + type: string + timeout: + format: duration + type: string + required: + - allowedToFail + - organizationRef + - script + - timeout + type: object + status: + description: ActionStatus defines the observed state of Action + properties: + actionId: + default: "" + type: string + 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 + required: + - actionId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/ops/chart/crds/flow-crd.yaml b/ops/chart/crds/flow-crd.yaml new file mode 100644 index 0000000..387b01f --- /dev/null +++ b/ops/chart/crds/flow-crd.yaml @@ -0,0 +1,221 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: flows.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: Flow + listKind: FlowList + plural: flows + singular: flow + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Flow is the Schema for the flows 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: FlowSpec defines the desired state of Flow + properties: + actionRefs: + items: + 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 + type: array + flowType: + enum: + - FLOW_TYPE_EXTERNAL_AUTHENTICATION + - "1" + - "2" + - "3" + - "4" + type: string + organizationRef: + 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 + triggerType: + enum: + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - "1" + - "2" + - "3" + - "4" + - "5" + - "6" + type: string + required: + - actionRefs + - flowType + - organizationRef + - triggerType + type: object + status: + description: FlowStatus defines the observed state of Flow + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/ops/chart/templates/manager-rbac.yaml b/ops/chart/templates/manager-rbac.yaml index 6e7066a..dde8e86 100644 --- a/ops/chart/templates/manager-rbac.yaml +++ b/ops/chart/templates/manager-rbac.yaml @@ -170,6 +170,32 @@ rules: - list - patch - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/finalizers + verbs: + - update +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/status + verbs: + - get + - patch + - update - apiGroups: - zitadel.topmanage.com resources: @@ -196,6 +222,32 @@ rules: - get - patch - update +- apiGroups: + - zitadel.topmanage.com + resources: + - flows + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/finalizers + verbs: + - update +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/status + verbs: + - get + - patch + - update - apiGroups: - zitadel.topmanage.com resources: diff --git a/src/PROJECT b/src/PROJECT index 0edf841..da86cb5 100644 --- a/src/PROJECT +++ b/src/PROJECT @@ -65,4 +65,22 @@ resources: kind: APIApp 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: Action + 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: Flow + path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/src/api/v1alpha1/action_types.go b/src/api/v1alpha1/action_types.go new file mode 100644 index 0000000..033efb1 --- /dev/null +++ b/src/api/v1alpha1/action_types.go @@ -0,0 +1,109 @@ +/* +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" + "fmt" + "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. + +// ActionSpec defines the desired state of Action +type ActionSpec 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 + OrganizationRef OrganizationRef `json:"organizationRef"` + Script string `json:"script"` + // +kubebuilder:default=true + AllowedToFail bool `json:"allowedToFail"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + Timeout *metav1.Duration `json:"timeout"` +} + +// ActionStatus defines the observed state of Action +type ActionStatus 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="" + ActionId string `json:"actionId"` +} + +func (d *ActionStatus) 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 + +// Action is the Schema for the actions API +type Action struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ActionSpec `json:"spec,omitempty"` + Status ActionStatus `json:"status,omitempty"` +} + +func (d *Action) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Action) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Action) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver) (*ZitadelClusterRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } + + ref, err := org.ZitadelClusterRef(ctx, refresolver) + if err != nil { + return nil, err + } + return ref, nil +} + +//+kubebuilder:object:root=true + +// ActionList contains a list of Action +type ActionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Action `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Action{}, &ActionList{}) +} diff --git a/src/api/v1alpha1/flow_types.go b/src/api/v1alpha1/flow_types.go new file mode 100644 index 0000000..f337de4 --- /dev/null +++ b/src/api/v1alpha1/flow_types.go @@ -0,0 +1,106 @@ +/* +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" + "fmt" + "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. + +// FlowSpec defines the desired state of Flow +type FlowSpec 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 + OrganizationRef OrganizationRef `json:"organizationRef"` + // +kubebuilder:validation:Enum=FLOW_TYPE_EXTERNAL_AUTHENTICATION;"1";"2";"3";"4" + FlowType string `json:"flowType"` + // +kubebuilder:validation:Enum=TRIGGER_TYPE_POST_AUTHENTICATION;TRIGGER_TYPE_PRE_CREATION;TRIGGER_TYPE_POST_CREATION;TRIGGER_TYPE_POST_AUTHENTICATION;TRIGGER_TYPE_PRE_CREATION;TRIGGER_TYPE_POST_CREATION;"1";"2";"3";"4";"5";"6" + TriggerType string `json:"triggerType"` + ActionRefs []ActionRef `json:"actionRefs"` +} + +// FlowStatus defines the observed state of Flow +type FlowStatus 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"` +} + +func (d *FlowStatus) 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 + +// Flow is the Schema for the flows API +type Flow struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FlowSpec `json:"spec,omitempty"` + Status FlowStatus `json:"status,omitempty"` +} + +func (d *Flow) IsBeingDeleted() bool { + return !d.DeletionTimestamp.IsZero() +} + +func (d *Flow) IsReady() bool { + return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady) +} + +func (d *Flow) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver) (*ZitadelClusterRef, error) { + org, err := refresolver.OrganizationRef(ctx, &d.Spec.OrganizationRef, d.Namespace) + if err != nil { + return nil, err + } + if org.Status.OrgId == "" { + return nil, fmt.Errorf("Organization has not been created yet...") + } + + ref, err := org.ZitadelClusterRef(ctx, refresolver) + if err != nil { + return nil, err + } + return ref, nil +} + +//+kubebuilder:object:root=true + +// FlowList contains a list of Flow +type FlowList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Flow `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Flow{}, &FlowList{}) +} diff --git a/src/api/v1alpha1/ref_types.go b/src/api/v1alpha1/ref_types.go index 1eedd2b..62dae0f 100644 --- a/src/api/v1alpha1/ref_types.go +++ b/src/api/v1alpha1/ref_types.go @@ -38,3 +38,10 @@ type ProjectRef struct { // +operator-sdk:csv:customresourcedefinitions:type=spec corev1.ObjectReference `json:",inline"` } + +type ActionRef struct { + // ObjectReference is a reference to a object. + // +kubebuilder:validation:Required + // +operator-sdk:csv:customresourcedefinitions:type=spec + corev1.ObjectReference `json:",inline"` +} diff --git a/src/api/v1alpha1/refresolver.go b/src/api/v1alpha1/refresolver.go index 5dfa87c..c9c3455 100644 --- a/src/api/v1alpha1/refresolver.go +++ b/src/api/v1alpha1/refresolver.go @@ -63,6 +63,27 @@ func (r *RefResolver) OIDCAppRef(ctx context.Context, ref *OIDCAppRef, return &zitadel, nil } +func (r *RefResolver) ActionRef(ctx context.Context, ref *ActionRef, + namespace string) (*Action, error) { + if ref.Kind != "" && ref.Kind != "Action" { + return nil, fmt.Errorf("Unsupported reference kind: '%s'", ref.Kind) + } + + key := types.NamespacedName{ + Name: ref.Name, + Namespace: namespace, + } + if ref.Namespace != "" { + key.Namespace = ref.Namespace + } + + var zitadel Action + if err := r.client.Get(ctx, key, &zitadel); err != nil { + return nil, err + } + return &zitadel, nil +} + func (r *RefResolver) ProjectRef(ctx context.Context, ref *ProjectRef, namespace string) (*Project, error) { if ref.Kind != "" && ref.Kind != "Project" { diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 4dcfe0e..5b721f6 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -123,6 +123,124 @@ func (in *APIAppStatus) DeepCopy() *APIAppStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Action) DeepCopyInto(out *Action) { + *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 Action. +func (in *Action) DeepCopy() *Action { + if in == nil { + return nil + } + out := new(Action) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Action) 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 *ActionList) DeepCopyInto(out *ActionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Action, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionList. +func (in *ActionList) DeepCopy() *ActionList { + if in == nil { + return nil + } + out := new(ActionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActionList) 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 *ActionRef) DeepCopyInto(out *ActionRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionRef. +func (in *ActionRef) DeepCopy() *ActionRef { + if in == nil { + return nil + } + out := new(ActionRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionSpec) DeepCopyInto(out *ActionSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionSpec. +func (in *ActionSpec) DeepCopy() *ActionSpec { + if in == nil { + return nil + } + out := new(ActionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionStatus) DeepCopyInto(out *ActionStatus) { + *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 ActionStatus. +func (in *ActionStatus) DeepCopy() *ActionStatus { + if in == nil { + return nil + } + out := new(ActionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CrdbClusterRef) DeepCopyInto(out *CrdbClusterRef) { *out = *in @@ -154,6 +272,108 @@ func (in *DomainSettings) DeepCopy() *DomainSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flow) DeepCopyInto(out *Flow) { + *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 Flow. +func (in *Flow) DeepCopy() *Flow { + if in == nil { + return nil + } + out := new(Flow) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Flow) 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 *FlowList) DeepCopyInto(out *FlowList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Flow, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowList. +func (in *FlowList) DeepCopy() *FlowList { + if in == nil { + return nil + } + out := new(FlowList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlowList) 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 *FlowSpec) DeepCopyInto(out *FlowSpec) { + *out = *in + out.OrganizationRef = in.OrganizationRef + if in.ActionRefs != nil { + in, out := &in.ActionRefs, &out.ActionRefs + *out = make([]ActionRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowSpec. +func (in *FlowSpec) DeepCopy() *FlowSpec { + if in == nil { + return nil + } + out := new(FlowSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowStatus) DeepCopyInto(out *FlowStatus) { + *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 FlowStatus. +func (in *FlowStatus) DeepCopy() *FlowStatus { + if in == nil { + return nil + } + out := new(FlowStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Grant) DeepCopyInto(out *Grant) { *out = *in diff --git a/src/cmd/main.go b/src/cmd/main.go index 5151801..62d8d97 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -137,6 +137,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "APIApp") os.Exit(1) } + if err = controller.NewActionReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Action") + os.Exit(1) + } + if err = controller.NewFlowReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Flow") + 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_actions.yaml b/src/config/crd/bases/zitadel.topmanage.com_actions.yaml new file mode 100644 index 0000000..f295e32 --- /dev/null +++ b/src/config/crd/bases/zitadel.topmanage.com_actions.yaml @@ -0,0 +1,174 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: actions.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: Action + listKind: ActionList + plural: actions + singular: action + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Action is the Schema for the actions 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: ActionSpec defines the desired state of Action + properties: + allowedToFail: + default: true + type: boolean + organizationRef: + 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 + script: + type: string + timeout: + format: duration + type: string + required: + - allowedToFail + - organizationRef + - script + - timeout + type: object + status: + description: ActionStatus defines the observed state of Action + properties: + actionId: + default: "" + type: string + 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 + required: + - actionId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/src/config/crd/bases/zitadel.topmanage.com_flows.yaml b/src/config/crd/bases/zitadel.topmanage.com_flows.yaml new file mode 100644 index 0000000..79c2d74 --- /dev/null +++ b/src/config/crd/bases/zitadel.topmanage.com_flows.yaml @@ -0,0 +1,222 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: flows.zitadel.topmanage.com +spec: + group: zitadel.topmanage.com + names: + kind: Flow + listKind: FlowList + plural: flows + singular: flow + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Flow is the Schema for the flows 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: FlowSpec defines the desired state of Flow + properties: + actionRefs: + items: + 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 + type: array + flowType: + enum: + - FLOW_TYPE_EXTERNAL_AUTHENTICATION + - "1" + - "2" + - "3" + - "4" + type: string + organizationRef: + 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 + triggerType: + enum: + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - TRIGGER_TYPE_POST_AUTHENTICATION + - TRIGGER_TYPE_PRE_CREATION + - TRIGGER_TYPE_POST_CREATION + - "1" + - "2" + - "3" + - "4" + - "5" + - "6" + type: string + required: + - actionRefs + - flowType + - organizationRef + - triggerType + type: object + status: + description: FlowStatus defines the observed state of Flow + 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 + 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 08028de..278c493 100644 --- a/src/config/crd/kustomization.yaml +++ b/src/config/crd/kustomization.yaml @@ -8,6 +8,8 @@ resources: - bases/zitadel.topmanage.com_oidcapps.yaml - bases/zitadel.topmanage.com_machineusers.yaml - bases/zitadel.topmanage.com_apiapps.yaml +- bases/zitadel.topmanage.com_actions.yaml +- bases/zitadel.topmanage.com_flows.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -19,6 +21,8 @@ patchesStrategicMerge: #- patches/webhook_in_oidcapps.yaml #- patches/webhook_in_machineusers.yaml #- patches/webhook_in_apiapps.yaml +#- patches/webhook_in_actions.yaml +#- patches/webhook_in_flows.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -29,6 +33,8 @@ patchesStrategicMerge: #- patches/cainjection_in_oidcapps.yaml #- patches/cainjection_in_machineusers.yaml #- patches/cainjection_in_apiapps.yaml +#- patches/cainjection_in_actions.yaml +#- patches/cainjection_in_flows.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_actions.yaml b/src/config/crd/patches/cainjection_in_actions.yaml new file mode 100644 index 0000000..0b731e9 --- /dev/null +++ b/src/config/crd/patches/cainjection_in_actions.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: actions.zitadel.topmanage.com diff --git a/src/config/crd/patches/cainjection_in_flows.yaml b/src/config/crd/patches/cainjection_in_flows.yaml new file mode 100644 index 0000000..57b4d50 --- /dev/null +++ b/src/config/crd/patches/cainjection_in_flows.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: flows.zitadel.topmanage.com diff --git a/src/config/crd/patches/webhook_in_actions.yaml b/src/config/crd/patches/webhook_in_actions.yaml new file mode 100644 index 0000000..3841844 --- /dev/null +++ b/src/config/crd/patches/webhook_in_actions.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: actions.zitadel.topmanage.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/src/config/crd/patches/webhook_in_flows.yaml b/src/config/crd/patches/webhook_in_flows.yaml new file mode 100644 index 0000000..4b0187c --- /dev/null +++ b/src/config/crd/patches/webhook_in_flows.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: flows.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/action_editor_role.yaml b/src/config/rbac/action_editor_role.yaml new file mode 100644 index 0000000..2bb95fc --- /dev/null +++ b/src/config/rbac/action_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit actions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: action-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: action-editor-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - actions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/status + verbs: + - get diff --git a/src/config/rbac/action_viewer_role.yaml b/src/config/rbac/action_viewer_role.yaml new file mode 100644 index 0000000..5e1b269 --- /dev/null +++ b/src/config/rbac/action_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view actions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: action-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: action-viewer-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - actions + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/status + verbs: + - get diff --git a/src/config/rbac/flow_editor_role.yaml b/src/config/rbac/flow_editor_role.yaml new file mode 100644 index 0000000..a9f28b7 --- /dev/null +++ b/src/config/rbac/flow_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit flows. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flow-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: flow-editor-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - flows + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/status + verbs: + - get diff --git a/src/config/rbac/flow_viewer_role.yaml b/src/config/rbac/flow_viewer_role.yaml new file mode 100644 index 0000000..da5ef2c --- /dev/null +++ b/src/config/rbac/flow_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view flows. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flow-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: flow-viewer-role +rules: +- apiGroups: + - zitadel.topmanage.com + resources: + - flows + verbs: + - get + - list + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/status + verbs: + - get diff --git a/src/config/rbac/role.yaml b/src/config/rbac/role.yaml index 8e3f4a9..200a805 100644 --- a/src/config/rbac/role.yaml +++ b/src/config/rbac/role.yaml @@ -170,6 +170,32 @@ rules: - list - patch - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/finalizers + verbs: + - update +- apiGroups: + - zitadel.topmanage.com + resources: + - actions/status + verbs: + - get + - patch + - update - apiGroups: - zitadel.topmanage.com resources: @@ -196,6 +222,32 @@ rules: - get - patch - update +- apiGroups: + - zitadel.topmanage.com + resources: + - flows + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/finalizers + verbs: + - update +- apiGroups: + - zitadel.topmanage.com + resources: + - flows/status + verbs: + - get + - patch + - update - apiGroups: - zitadel.topmanage.com resources: diff --git a/src/config/samples/kustomization.yaml b/src/config/samples/kustomization.yaml index fe16a01..001b84c 100644 --- a/src/config/samples/kustomization.yaml +++ b/src/config/samples/kustomization.yaml @@ -6,4 +6,6 @@ resources: - zitadel_v1alpha1_oidcapp.yaml - zitadel_v1alpha1_machineuser.yaml - zitadel_v1alpha1_apiapp.yaml +- zitadel_v1alpha1_action.yaml +- zitadel_v1alpha1_flow.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/src/config/samples/zitadel_v1alpha1_action.yaml b/src/config/samples/zitadel_v1alpha1_action.yaml new file mode 100644 index 0000000..c851467 --- /dev/null +++ b/src/config/samples/zitadel_v1alpha1_action.yaml @@ -0,0 +1,12 @@ +apiVersion: zitadel.topmanage.com/v1alpha1 +kind: Action +metadata: + labels: + app.kubernetes.io/name: action + app.kubernetes.io/instance: action-sample + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: src + name: action-sample +spec: + # TODO(user): Add fields here diff --git a/src/config/samples/zitadel_v1alpha1_flow.yaml b/src/config/samples/zitadel_v1alpha1_flow.yaml new file mode 100644 index 0000000..3f9237d --- /dev/null +++ b/src/config/samples/zitadel_v1alpha1_flow.yaml @@ -0,0 +1,12 @@ +apiVersion: zitadel.topmanage.com/v1alpha1 +kind: Flow +metadata: + labels: + app.kubernetes.io/name: flow + app.kubernetes.io/instance: flow-sample + app.kubernetes.io/part-of: src + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: src + name: flow-sample +spec: + # TODO(user): Add fields here diff --git a/src/internal/controller/action_controller.go b/src/internal/controller/action_controller.go new file mode 100644 index 0000000..355fedb --- /dev/null +++ b/src/internal/controller/action_controller.go @@ -0,0 +1,191 @@ +/* +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 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/middleware" + pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + durationpb "google.golang.org/protobuf/types/known/durationpb" + "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" +) + +// ActionReconciler reconciles a Action object +type ActionReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *ActionReconciler { + return &ActionReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=actions/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 *ActionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var Action zitadelv1alpha1.Action + if err := r.Get(ctx, req.NamespacedName, &Action); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedActionReconciler(r.Client, r.RefResolver, r.Builder, &Action) + wf := newWrappedActionFinalizer(r.Client, &Action, r.RefResolver) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &Action) + if err != nil { + return result, fmt.Errorf("error reconciling in ActionReconciler: %v", err) + } + return result, nil +} + +type wrappedActionReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + Action *zitadelv1alpha1.Action + Builder *builder.Builder +} + +func newWrappedActionReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + Action *zitadelv1alpha1.Action) zitadel.WrappedReconciler { + return &wrappedActionReconciler{ + Client: client, + refResolver: refResolver, + Action: Action, + Builder: builder, + } +} + +type actionReoncilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + +func (wr *wrappedActionReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []actionReoncilePhase{ + { + Name: "action", + Reconcile: wr.reconcileAction, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedActionReconciler) reconcileAction(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.Action.Spec.OrganizationRef, wr.Action.Namespace) + if err != nil { + return err + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + + if wr.Action.Status.ActionId != "" { + p, err := ztdClient.GetAction(ctx, &pb.GetActionRequest{Id: wr.Action.Status.ActionId}) + if p != nil { + _, err := ztdClient.UpdateAction(ctx, + &pb.UpdateActionRequest{ + Id: p.Action.Id, + Name: wr.Action.Name, + Script: wr.Action.Spec.Script, + Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration), + AllowedToFail: wr.Action.Spec.AllowedToFail, + }, + ) + + if err != nil { + if !strings.Contains(err.Error(), "No changes") { + return fmt.Errorf("Error updating Action: %v", err) + } + } + return nil + } + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return fmt.Errorf("Error getting Action: %v", err) + } + } + } + + resp, err := ztdClient.CreateAction(ctx, + &pb.CreateActionRequest{ + Name: wr.Action.Name, + Script: wr.Action.Spec.Script, + Timeout: durationpb.New(wr.Action.Spec.Timeout.Duration), + AllowedToFail: wr.Action.Spec.AllowedToFail, + }, + ) + + if err != nil { + if strings.Contains(err.Error(), "AlreadyExists") { + return nil + } + return fmt.Errorf("error creating action in Zitadel: %v", err) + } + patch := ctrlClient.MergeFrom(wr.Action.DeepCopy()) + wr.Action.Status.ActionId = resp.Id + return wr.Client.Status().Patch(ctx, wr.Action, patch) +} + +func (wr *wrappedActionReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.Action.DeepCopy()) + patcher(&wr.Action.Status) + + if err := wr.Client.Status().Patch(ctx, wr.Action, patch); err != nil { + return fmt.Errorf("error patching Action status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ActionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Action{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/action_controller_finalizer.go b/src/internal/controller/action_controller_finalizer.go new file mode 100644 index 0000000..545010e --- /dev/null +++ b/src/internal/controller/action_controller_finalizer.go @@ -0,0 +1,94 @@ +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" + "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" + 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 ( + actionFinalizerName = "action.zitadel.topmanage.com/action" +) + +type wrappedActionFinalizer struct { + client.Client + action *zitadelv1alpha1.Action + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedActionFinalizer(client client.Client, action *zitadelv1alpha1.Action, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { + return &wrappedActionFinalizer{ + Client: client, + action: action, + refresolver: refresolver, + } +} + +func (wf *wrappedActionFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) { + controllerutil.AddFinalizer(action, actionFinalizerName) + }) +} + +func (wf *wrappedActionFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.action, func(action *zitadelv1alpha1.Action) { + controllerutil.RemoveFinalizer(wf.action, actionFinalizerName) + }) +} + +func (wr *wrappedActionFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.action, actionFinalizerName) +} + +func (wf *wrappedActionFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + if wf.action.Status.ActionId == "" { + return nil + } + org, err := wf.refresolver.OrganizationRef(ctx, &wf.action.Spec.OrganizationRef, wf.action.Namespace) + if err != nil { + return err + } + { + + _, err := ztdClient.GetAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetActionRequest{Id: wf.action.Status.ActionId}) + if err != nil { + if strings.Contains(err.Error(), `doesn't exist`) { + return nil + } + return err + } + } + _, err = ztdClient.DeleteAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.DeleteActionRequest{Id: wf.action.Status.ActionId}) + if err != nil { + return err + } + return nil +} + +func (wr *wrappedActionFinalizer) patch(ctx context.Context, action *zitadelv1alpha1.Action, + patchFn func(*zitadelv1alpha1.Action)) error { + patch := ctrlClient.MergeFrom(action.DeepCopy()) + patchFn(action) + + if err := wr.Client.Patch(ctx, action, patch); err != nil { + return fmt.Errorf("error patching Action finalizer: %v", err) + } + return nil +} diff --git a/src/internal/controller/flow_controller.go b/src/internal/controller/flow_controller.go new file mode 100644 index 0000000..90fbbc4 --- /dev/null +++ b/src/internal/controller/flow_controller.go @@ -0,0 +1,164 @@ +/* +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 controller + +import ( + "context" + "fmt" + "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/middleware" + pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" +) + +// FlowReconciler reconciles a Flow object +type FlowReconciler struct { + client.Client + RefResolver *zitadelv1alpha1.RefResolver + ConditionReady *condition.Ready + RequeueInterval time.Duration + Builder *builder.Builder +} + +func NewFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready, + requeueInterval time.Duration) *FlowReconciler { + return &FlowReconciler{ + Client: client, + RefResolver: refResolver, + ConditionReady: conditionReady, + RequeueInterval: requeueInterval, + Builder: builder, + } +} + +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=flows/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 *FlowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var Flow zitadelv1alpha1.Flow + if err := r.Get(ctx, req.NamespacedName, &Flow); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + wr := newWrappedFlowReconciler(r.Client, r.RefResolver, r.Builder, &Flow) + wf := newWrappedFlowFinalizer(r.Client, &Flow, r.RefResolver) + tf := zitadel.NewZitadelFinalizer(r.Client, wf) + tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval) + + result, err := tr.Reconcile(ctx, &Flow) + if err != nil { + return result, fmt.Errorf("error reconciling in FlowReconciler: %v", err) + } + return result, nil +} + +type wrappedFlowReconciler struct { + client.Client + refResolver *zitadelv1alpha1.RefResolver + Flow *zitadelv1alpha1.Flow + Builder *builder.Builder +} + +func newWrappedFlowReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, + Flow *zitadelv1alpha1.Flow) zitadel.WrappedReconciler { + return &wrappedFlowReconciler{ + Client: client, + refResolver: refResolver, + Flow: Flow, + Builder: builder, + } +} + +type flowReoncilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + +func (wr *wrappedFlowReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []flowReoncilePhase{ + { + Name: "flow", + Reconcile: wr.reconcileFlow, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedFlowReconciler) reconcileFlow(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.Flow.Spec.OrganizationRef, wr.Flow.Namespace) + if err != nil { + return err + } + ctx = middleware.SetOrgID(ctx, org.Status.OrgId) + + actionIds := []string{} + for _, actionRef := range wr.Flow.Spec.ActionRefs { + action, err := wr.refResolver.ActionRef(ctx, &actionRef, wr.Flow.Namespace) + if err != nil { + return fmt.Errorf("Error resolving action reference: %v", err) + } + if action.Status.ActionId == "" { + return fmt.Errorf("Action with name: %s not ready for trigger", action.Name) + } + actionIds = append(actionIds, action.Status.ActionId) + } + + _, err = ztdClient.SetTriggerActions(ctx, &pb.SetTriggerActionsRequest{ + FlowType: wr.Flow.Spec.FlowType, + TriggerType: wr.Flow.Spec.TriggerType, + ActionIds: actionIds, + }) + if err != nil { + return fmt.Errorf("Error triggering action flow: %v", err) + } + return nil +} + +func (wr *wrappedFlowReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { + patch := client.MergeFrom(wr.Flow.DeepCopy()) + patcher(&wr.Flow.Status) + + if err := wr.Client.Status().Patch(ctx, wr.Flow, patch); err != nil { + return fmt.Errorf("error patching Flow status: %v", err) + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FlowReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&zitadelv1alpha1.Flow{}). + WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}). + Complete(r) +} diff --git a/src/internal/controller/flow_controller_finalizer.go b/src/internal/controller/flow_controller_finalizer.go new file mode 100644 index 0000000..696e71e --- /dev/null +++ b/src/internal/controller/flow_controller_finalizer.go @@ -0,0 +1,82 @@ +package controller + +import ( + 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" + "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" + 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 ( + flowFinalizerName = "flow.zitadel.topmanage.com/flow" +) + +type wrappedFlowFinalizer struct { + client.Client + flow *zitadelv1alpha1.Flow + refresolver *zitadelv1alpha1.RefResolver +} + +func newWrappedFlowFinalizer(client client.Client, flow *zitadelv1alpha1.Flow, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer { + return &wrappedFlowFinalizer{ + Client: client, + flow: flow, + refresolver: refresolver, + } +} + +func (wf *wrappedFlowFinalizer) AddFinalizer(ctx context.Context) error { + if wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) { + controllerutil.AddFinalizer(flow, flowFinalizerName) + }) +} + +func (wf *wrappedFlowFinalizer) RemoveFinalizer(ctx context.Context) error { + if !wf.ContainsFinalizer() { + return nil + } + return wf.patch(ctx, wf.flow, func(flow *zitadelv1alpha1.Flow) { + controllerutil.RemoveFinalizer(wf.flow, flowFinalizerName) + }) +} + +func (wr *wrappedFlowFinalizer) ContainsFinalizer() bool { + return controllerutil.ContainsFinalizer(wr.flow, flowFinalizerName) +} + +func (wf *wrappedFlowFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error { + org, err := wf.refresolver.OrganizationRef(ctx, &wf.flow.Spec.OrganizationRef, wf.flow.Namespace) + if err != nil { + return err + } + + _, err = ztdClient.ClearFlow(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.ClearFlowRequest{ + Type: wf.flow.Spec.FlowType, + }) + if err != nil { + return err + } + return nil +} + +func (wr *wrappedFlowFinalizer) patch(ctx context.Context, flow *zitadelv1alpha1.Flow, + patchFn func(*zitadelv1alpha1.Flow)) error { + patch := ctrlClient.MergeFrom(flow.DeepCopy()) + patchFn(flow) + + if err := wr.Client.Patch(ctx, flow, patch); err != nil { + return fmt.Errorf("error patching Flow finalizer: %v", err) + } + return nil +} From 51bff475e9beaf1776057b0ece85dbdc555fef15 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Tue, 18 Jun 2024 14:23:27 -0500 Subject: [PATCH 2/5] Add no changes to flow [ZITADOPER-4] --- src/internal/controller/flow_controller.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/internal/controller/flow_controller.go b/src/internal/controller/flow_controller.go index 90fbbc4..7aa5963 100644 --- a/src/internal/controller/flow_controller.go +++ b/src/internal/controller/flow_controller.go @@ -19,6 +19,7 @@ package controller import ( "context" "fmt" + "strings" "time" zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" @@ -140,7 +141,9 @@ func (wr *wrappedFlowReconciler) reconcileFlow(ctx context.Context, ztdClient *m ActionIds: actionIds, }) if err != nil { - return fmt.Errorf("Error triggering action flow: %v", err) + if !strings.Contains(err.Error(), "No Changes") { + return fmt.Errorf("Error triggering action flow: %v", err) + } } return nil } From abe0c7484d7ae6582a4dd3accf69326b2f725446 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Tue, 18 Jun 2024 14:24:27 -0500 Subject: [PATCH 3/5] Add not found to project [ZITADOPER-4] --- src/internal/controller/project_controller_finalizer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/controller/project_controller_finalizer.go b/src/internal/controller/project_controller_finalizer.go index 7122bb3..c7c71f3 100644 --- a/src/internal/controller/project_controller_finalizer.go +++ b/src/internal/controller/project_controller_finalizer.go @@ -69,7 +69,7 @@ func (wf *wrappedProjectFinalizer) Reconcile(ctx context.Context, ztdClient *man _, err := ztdClient.GetProjectByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetProjectByIDRequest{Id: wf.project.Status.ProjectId}) if err != nil { - if strings.Contains(err.Error(), `doesn't exist`) { + if strings.Contains(err.Error(), `not found`) { return nil } return err From da81d6d87240885588fa2fb035451941c2c496f7 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Wed, 19 Jun 2024 14:56:08 -0500 Subject: [PATCH 4/5] fix action not found [ZITADOPER-4] --- src/internal/controller/action_controller_finalizer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/controller/action_controller_finalizer.go b/src/internal/controller/action_controller_finalizer.go index 545010e..e81be00 100644 --- a/src/internal/controller/action_controller_finalizer.go +++ b/src/internal/controller/action_controller_finalizer.go @@ -69,7 +69,7 @@ func (wf *wrappedActionFinalizer) Reconcile(ctx context.Context, ztdClient *mana _, err := ztdClient.GetAction(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetActionRequest{Id: wf.action.Status.ActionId}) if err != nil { - if strings.Contains(err.Error(), `doesn't exist`) { + if strings.Contains(err.Error(), `not found`) { return nil } return err From 70ea5b017c78ca5135bc7958a21ebec59559ea06 Mon Sep 17 00:00:00 2001 From: Haim Kortovich Date: Fri, 21 Jun 2024 11:41:26 -0500 Subject: [PATCH 5/5] Requeue zitadel every 5 minutes [ZITADOPER-4] --- src/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/main.go b/src/cmd/main.go index 62d8d97..255a2e4 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -102,7 +102,7 @@ func main() { serviceReconciler := service.NewServiceReconciler(client) refResolver := zitadelv1alpha1.NewRefResolver(client) conditionReady := conditions.NewReady() - requeueZitadel := 30 * time.Second + requeueZitadel := 5 * time.Minute if err = (&controller.ZitadelClusterReconciler{ Client: client, Scheme: scheme,