diff --git a/apis/helmproviderstacks/definition.yaml b/apis/helmproviderstacks/definition.yaml index c74c8b5..b8d1781 100644 --- a/apis/helmproviderstacks/definition.yaml +++ b/apis/helmproviderstacks/definition.yaml @@ -78,6 +78,32 @@ spec: type: string requestsMemory: type: string + aws: + description: Optional EKS Pod Identity for private ECR chart pulls. + type: object + x-kubernetes-validations: + - rule: "!has(self.enabled) || !self.enabled || (has(self.region) && has(self.ecrRepositoryArns) && size(self.ecrRepositoryArns) > 0)" + message: AWS chart access requires region and at least one ECR repository ARN. + properties: + enabled: + type: boolean + default: false + region: + type: string + minLength: 1 + ecrRepositoryArns: + type: array + items: + type: string + pattern: '^arn:[^:]+:ecr:[^:]+:[0-9]{12}:repository/.+$' + providerConfigRef: + type: object + properties: + name: + type: string + kind: + type: string + enum: [ProviderConfig, ClusterProviderConfig] nodePool: description: Schedule this provider runtime onto the CrossplaneStack NodePool. This does not create or rename the NodePool; CrossplaneStack defaults it to hops-crossplane. type: object diff --git a/docs/runtime-service-accounts.md b/docs/runtime-service-accounts.md new file mode 100644 index 0000000..08cd55f --- /dev/null +++ b/docs/runtime-service-accounts.md @@ -0,0 +1,46 @@ +# Runtime service account ownership + +Provider stacks manage their stable service accounts through Kubernetes Objects. +DeploymentRuntimeConfigs reference those accounts with +`deploymentTemplate.spec.template.spec.serviceAccountName`. This preserves the +names used by Pod Identity and Kubernetes RBAC while allowing multiple package +revisions, and the AWS family providers, to use the same account. + +Functions use Crossplane's generated per-revision accounts; they have no +external bindings that require a stable account name. + +Do not set `serviceAccountTemplate.metadata.name` for shared accounts. Crossplane +then tries to make each package revision their controller, and Kubernetes rejects +the competing controller owner references. + +## Private ECR Helm charts + +`HelmProviderStack.spec.aws` optionally enables EKS Pod Identity for the Helm +runtime. Set `enabled: true`, `region`, and `ecrRepositoryArns` to the private +chart repositories it may read. `aws.providerConfigRef` selects the AWS provider +configuration and defaults to the cluster name. No AWS identity is created when +this option is omitted or disabled. + +The stack binds the role to its own stable runtime service account. Its policy +allows ECR token generation and read access only to the supplied repositories; +it does not grant repository creation, upstream import, or image publication. +This is separate from Crossplane package-manager authentication. + +Application environments should create their own namespaced Helm +`ProviderConfig` with `credentials.source: InjectedIdentity`. The shared +Helm runtime's AWS access belongs to the provider stack. + +Existing Helm pods must be recreated after the Pod Identity association is ready +to receive EKS credential injection. + +## Migrating existing installations + +Existing accounts can still carry an owner reference to a package revision from +the previous templates. After the new runtime configuration and ServiceAccount +Object have reconciled, inspect each affected account and remove only that +obsolete ProviderRevision controller reference. Preserve unrelated metadata and +any new owner references. Do not delete the account: existing RBAC and Pod +Identity associations refer to its stable name. + +Verify all provider and function packages become Healthy before removing old +registry infrastructure or pruning inactive package revisions. diff --git a/examples/helmproviderstacks/full.yaml b/examples/helmproviderstacks/full.yaml index c9d3d4b..a8b78b5 100644 --- a/examples/helmproviderstacks/full.yaml +++ b/examples/helmproviderstacks/full.yaml @@ -5,6 +5,14 @@ metadata: namespace: default spec: clusterName: my-cluster + aws: + enabled: true + region: us-east-2 + providerConfigRef: + name: my-cluster + kind: ProviderConfig + ecrRepositoryArns: + - arn:aws:ecr:us-east-2:123456789012:repository/team/charts/* kubernetesProviderConfigRef: name: my-cluster runtimeConfig: diff --git a/functions/aws-provider/205-service-account.yaml.gotmpl b/functions/aws-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..cd581d6 --- /dev/null +++ b/functions/aws-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,64 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-aws + annotations: + {{ setResourceNameAnnotation "sa-aws" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.podIdentity.saName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-family-aws" }} +{{- range $subName, $_ := $state.aws.selectedProviders }} +{{- $consumers = append $consumers (printf "provider-aws-%s" $subName) }} +{{- end }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-aws" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-aws + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-aws-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-aws + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/aws-provider/210-drc-aws.yaml.gotmpl b/functions/aws-provider/210-drc-aws.yaml.gotmpl index 9e010cf..8c3c7ae 100644 --- a/functions/aws-provider/210-drc-aws.yaml.gotmpl +++ b/functions/aws-provider/210-drc-aws.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.podIdentity.saName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.podIdentity.saName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/aws-provider/230-provider-aws-sub-providers.yaml.gotmpl b/functions/aws-provider/230-provider-aws-sub-providers.yaml.gotmpl index b4f6b0c..d988c2d 100644 --- a/functions/aws-provider/230-provider-aws-sub-providers.yaml.gotmpl +++ b/functions/aws-provider/230-provider-aws-sub-providers.yaml.gotmpl @@ -32,6 +32,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.podIdentity.saName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.podIdentity.saName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/functions/210-drc-functions.yaml.gotmpl b/functions/functions/210-drc-functions.yaml.gotmpl index 448074f..1f9408f 100644 --- a/functions/functions/210-drc-functions.yaml.gotmpl +++ b/functions/functions/210-drc-functions.yaml.gotmpl @@ -41,9 +41,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $fn.name }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/github-provider/205-service-account.yaml.gotmpl b/functions/github-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..998ff62 --- /dev/null +++ b/functions/github-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-github + annotations: + {{ setResourceNameAnnotation "sa-github" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-github" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-github" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-github + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-github-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-github + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/github-provider/210-drc-github.yaml.gotmpl b/functions/github-provider/210-drc-github.yaml.gotmpl index aafb981..f534de2 100644 --- a/functions/github-provider/210-drc-github.yaml.gotmpl +++ b/functions/github-provider/210-drc-github.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/helm-provider/005-state-aws.yaml.gotmpl b/functions/helm-provider/005-state-aws.yaml.gotmpl new file mode 100644 index 0000000..cb0e1d6 --- /dev/null +++ b/functions/helm-provider/005-state-aws.yaml.gotmpl @@ -0,0 +1,19 @@ +# code: language=yaml +{{- $aws := $spec.aws | default dict }} +{{- $awsConfig := $aws.providerConfigRef | default dict }} +{{- $state = set $state "aws" (dict + "enabled" ($aws.enabled | default false) + "region" ($aws.region | default "") + "ecrRepositoryArns" ($aws.ecrRepositoryArns | default list) + "providerConfigRef" (dict + "name" ($awsConfig.name | default $state.clusterName) + "kind" ($awsConfig.kind | default "ProviderConfig") + ) +) }} +{{- $entry := get ($.observed.resources | default dict) "helm-pod-identity" | default dict }} +{{- $resource := $entry.resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- $state = set $state "observed" (merge $state.observed (dict "podIdentity" (dict "ready" $ready))) }} diff --git a/functions/helm-provider/010-state-status.yaml.gotmpl b/functions/helm-provider/010-state-status.yaml.gotmpl index 7826fea..e2cb149 100644 --- a/functions/helm-provider/010-state-status.yaml.gotmpl +++ b/functions/helm-provider/010-state-status.yaml.gotmpl @@ -1,7 +1,7 @@ # code: language=yaml {{- $providerReady := $state.observed.provider.ready | default false }} {{- $status := dict - "ready" $providerReady + "ready" (and $providerReady (or (not $state.aws.enabled) $state.observed.podIdentity.ready)) "provider" (dict "ready" $providerReady ) diff --git a/functions/helm-provider/200-pod-identity.yaml.gotmpl b/functions/helm-provider/200-pod-identity.yaml.gotmpl new file mode 100644 index 0000000..0f8a99c --- /dev/null +++ b/functions/helm-provider/200-pod-identity.yaml.gotmpl @@ -0,0 +1,60 @@ +# code: language=yaml +{{- if and $state.enabled $state.aws.enabled }} +--- +apiVersion: aws.hops.ops.com.ai/v1alpha1 +kind: PodIdentity +metadata: + name: {{ $state.name }}-helm-ecr + annotations: + {{ setResourceNameAnnotation "helm-pod-identity" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + clusterName: {{ $state.clusterName }} + region: {{ $state.aws.region | quote }} + providerConfigRef: {{ $state.aws.providerConfigRef | toJson }} + serviceAccount: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + inlinePolicy: + - name: pull-ecr-charts + policy: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ecr:GetAuthorizationToken", + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": ["ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"], + "Resource": {{ $state.aws.ecrRepositoryArns | toJson }} + } + ] + } + tags: {{ $state.tags | toJson }} +{{- if and $state.observed.provider.ready $state.observed.podIdentity.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.name }}-delete-helm-provider-before-pod-identity + annotations: + {{ setResourceNameAnnotation "usage-helm-pod-identity" }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: aws.hops.ops.com.ai/v1alpha1 + kind: PodIdentity + resourceRef: + name: {{ $state.name }}-helm-ecr + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: provider-helm +{{- end }} +{{- end }} diff --git a/functions/helm-provider/205-service-account.yaml.gotmpl b/functions/helm-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..c825f24 --- /dev/null +++ b/functions/helm-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-helm + annotations: + {{ setResourceNameAnnotation "sa-helm" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-helm" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-helm" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-helm + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-helm-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-helm + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/helm-provider/210-drc-helm.yaml.gotmpl b/functions/helm-provider/210-drc-helm.yaml.gotmpl index e1181ac..858ea3a 100644 --- a/functions/helm-provider/210-drc-helm.yaml.gotmpl +++ b/functions/helm-provider/210-drc-helm.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/kubernetes-provider/205-service-account.yaml.gotmpl b/functions/kubernetes-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..66d5735 --- /dev/null +++ b/functions/kubernetes-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-kubernetes + annotations: + {{ setResourceNameAnnotation "sa-kubernetes" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-kubernetes" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-kubernetes" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-kubernetes + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-kubernetes-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-kubernetes + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/kubernetes-provider/210-drc-kubernetes.yaml.gotmpl b/functions/kubernetes-provider/210-drc-kubernetes.yaml.gotmpl index 9fa54ae..505ad3b 100644 --- a/functions/kubernetes-provider/210-drc-kubernetes.yaml.gotmpl +++ b/functions/kubernetes-provider/210-drc-kubernetes.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/listmonk-provider/205-service-account.yaml.gotmpl b/functions/listmonk-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..a414ac4 --- /dev/null +++ b/functions/listmonk-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-listmonk + annotations: + {{ setResourceNameAnnotation "sa-listmonk" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-listmonk" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-listmonk" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-listmonk + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-listmonk-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-listmonk + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/listmonk-provider/210-drc-listmonk.yaml.gotmpl b/functions/listmonk-provider/210-drc-listmonk.yaml.gotmpl index 6eb2c63..bd4ff42 100644 --- a/functions/listmonk-provider/210-drc-listmonk.yaml.gotmpl +++ b/functions/listmonk-provider/210-drc-listmonk.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/openpanel-provider/205-service-account.yaml.gotmpl b/functions/openpanel-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..fbc9ee9 --- /dev/null +++ b/functions/openpanel-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-openpanel + annotations: + {{ setResourceNameAnnotation "sa-openpanel" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-openpanel" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-openpanel" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-openpanel + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-openpanel-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-openpanel + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/openpanel-provider/210-drc-openpanel.yaml.gotmpl b/functions/openpanel-provider/210-drc-openpanel.yaml.gotmpl index 0cf3b42..f204d01 100644 --- a/functions/openpanel-provider/210-drc-openpanel.yaml.gotmpl +++ b/functions/openpanel-provider/210-drc-openpanel.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/functions/zitadel-provider/205-service-account.yaml.gotmpl b/functions/zitadel-provider/205-service-account.yaml.gotmpl new file mode 100644 index 0000000..7a21b45 --- /dev/null +++ b/functions/zitadel-provider/205-service-account.yaml.gotmpl @@ -0,0 +1,61 @@ +# code: language=yaml +{{- if $state.enabled }} +--- +apiVersion: kubernetes.m.crossplane.io/v1alpha1 +kind: Object +metadata: + name: sa-zitadel + annotations: + {{ setResourceNameAnnotation "sa-zitadel" }} + labels: {{ $state.labels | toJson }} +spec: + managementPolicies: {{ $state.managementPolicies | toJson }} + forProvider: + manifest: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: {{ $state.provider.runtimeConfigName }} + namespace: {{ $state.crossplane.namespace }} + labels: {{ $state.labels | toJson }} + providerConfigRef: + name: {{ $state.kubernetesProviderConfigRef.name }} + kind: {{ $state.kubernetesProviderConfigRef.kind }} +{{- /* Keep the stable account until all package Objects are removed. */}} +{{- $consumers := list "provider-zitadel" }} +{{- $observed := $.observed.resources | default dict }} +{{- $account := (get $observed "sa-zitadel" | default dict).resource | default dict }} +{{- $accountReady := false }} +{{- range ($account.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $accountReady = true }}{{- end }} +{{- end }} +{{- range $consumer := $consumers }} +{{- $resource := (get $observed $consumer | default dict).resource | default dict }} +{{- $ready := false }} +{{- range ($resource.status | default dict).conditions | default list }} +{{- if and (eq .type "Ready") (eq .status "True") }}{{- $ready = true }}{{- end }} +{{- end }} +{{- if and $accountReady $ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: delete-{{ $consumer }}-before-sa-zitadel + annotations: + {{ setResourceNameAnnotation (printf "usage-sa-zitadel-%s" $consumer) }} + labels: {{ $state.labels | toJson }} +spec: + replayDeletion: true + of: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: sa-zitadel + by: + apiVersion: kubernetes.m.crossplane.io/v1alpha1 + kind: Object + resourceRef: + name: {{ $consumer }} +{{- end }} +{{- end }} +{{- end }} diff --git a/functions/zitadel-provider/210-drc-zitadel.yaml.gotmpl b/functions/zitadel-provider/210-drc-zitadel.yaml.gotmpl index a9062c5..788863a 100644 --- a/functions/zitadel-provider/210-drc-zitadel.yaml.gotmpl +++ b/functions/zitadel-provider/210-drc-zitadel.yaml.gotmpl @@ -25,6 +25,7 @@ spec: selector: {} template: spec: + serviceAccountName: {{ $state.provider.runtimeConfigName }} containers: - name: package-runtime resources: @@ -51,9 +52,6 @@ spec: nodeSelector: {{ $state.scheduling.nodeSelector | toJson }} tolerations: {{ $state.scheduling.tolerations | toJson }} {{- end }} - serviceAccountTemplate: - metadata: - name: {{ $state.provider.runtimeConfigName }} providerConfigRef: name: {{ $state.kubernetesProviderConfigRef.name }} kind: {{ $state.kubernetesProviderConfigRef.kind }} diff --git a/tests/test-aws-provider-stack/main.k b/tests/test-aws-provider-stack/main.k index 8d0b421..68fd8a2 100644 --- a/tests/test-aws-provider-stack/main.k +++ b/tests/test-aws-provider-stack/main.k @@ -23,6 +23,21 @@ _items = [ } } assertResources = [ + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "sa-aws" + spec.forProvider.manifest = { + kind = "ServiceAccount" + metadata = {name = "my-cluster-crossplane-aws", namespace = "crossplane-system"} + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "drc-aws" + spec.forProvider.manifest.spec.deploymentTemplate.spec.template.spec.serviceAccountName = "my-cluster-crossplane-aws" + } { apiVersion = "aws.hops.ops.com.ai/v1alpha1" kind = "PodIdentity" diff --git a/tests/test-helm-provider-stack/main.k b/tests/test-helm-provider-stack/main.k index 3925859..cb0cc2a 100644 --- a/tests/test-helm-provider-stack/main.k +++ b/tests/test-helm-provider-stack/main.k @@ -28,6 +28,16 @@ _items = [ spec.forProvider.manifest = { apiVersion = "pkg.crossplane.io/v1beta1" kind = "DeploymentRuntimeConfig" + spec.deploymentTemplate.spec.template.spec.serviceAccountName = "my-cluster-helm" + } + } + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata.name = "sa-helm" + spec.forProvider.manifest = { + kind = "ServiceAccount" + metadata = {name = "my-cluster-helm", namespace = "crossplane-system"} } } { @@ -150,4 +160,82 @@ _items = [ } ] -items = _items +items = _items + [ + metav1alpha1.CompositionTest { + metadata.name = "ecr-identity-uses-runtime-account-and-aws-config" + spec = { + compositionPath = _composition + xrdPath = _xrd + timeoutSeconds = 60 + validate = False + xr = { + apiVersion = "crossplane.hops.ops.com.ai/v1alpha1" + kind = "HelmProviderStack" + metadata.name = "helm" + spec = { + clusterName = "my-cluster" + crossplaneNamespace = "custom-crossplane" + aws = { + enabled = True + region = "us-east-2" + providerConfigRef = {name = "aws-admin", kind = "ClusterProviderConfig"} + ecrRepositoryArns = ["arn:aws:ecr:us-east-2:123456789012:repository/team/charts/*"] + } + } + } + assertResources = [ + { + apiVersion = "aws.hops.ops.com.ai/v1alpha1" + kind = "PodIdentity" + metadata.name = "helm-helm-ecr" + spec = { + clusterName = "my-cluster" + region = "us-east-2" + providerConfigRef = {name = "aws-admin", kind = "ClusterProviderConfig"} + serviceAccount = {name = "my-cluster-helm", namespace = "custom-crossplane"} + } + } + ] + } + } + metav1alpha1.CompositionTest { + metadata.name = "account-outlives-ready-provider" + spec = { + compositionPath = _composition + xrdPath = _xrd + timeoutSeconds = 60 + validate = False + xr = { + apiVersion = "crossplane.hops.ops.com.ai/v1alpha1" + kind = "HelmProviderStack" + metadata.name = "helm" + spec.clusterName = "my-cluster" + metadata.namespace = "default" + } + observedResources = [ + { + apiVersion = "kubernetes.m.crossplane.io/v1alpha1" + kind = "Object" + metadata = { + name = resourceName + namespace = "default" + annotations = {"crossplane.io/composition-resource-name" = resourceName} + } + status.conditions = [{type = "Ready", status = "True"}] + } for resourceName in ["sa-helm", "provider-helm"] + ] + assertResources = [ + { + apiVersion = "protection.crossplane.io/v1beta1" + kind = "Usage" + metadata.name = "delete-provider-helm-before-sa-helm" + spec = { + of.resourceRef.name = "sa-helm" + by.resourceRef.name = "provider-helm" + replayDeletion = True + } + } + ] + } + } +]