Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add Crossplane AWS support for automated S3 bucket provisioning for CNPG backups.

### Changed

- migrated upstream helm chart to grafana-community
Expand Down
52 changes: 52 additions & 0 deletions helm/grafana/templates/cnpg/crossplane/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{{/*
Crossplane enabled check
*/}}
{{- define "grafana.crossplane.enabled" -}}
{{- if and .Values.postgresqlCluster.crossplane.enabled .Values.postgresqlCluster.crossplane.clusterName -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}

{{/*
Crossplane is AWS/CAPA
*/}}
{{- define "grafana.crossplane.isAWS" -}}
{{- if eq .Values.postgresqlCluster.crossplane.provider "aws" -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}

{{/*
Merge tags from cluster CR with user-provided tags
Returns tags as a map: {foo: "bar"}
*/}}
{{- define "grafana.crossplane.tags" -}}
{{- $clusterName := .Values.postgresqlCluster.crossplane.clusterName -}}
{{- $clusterNamespace := .Values.postgresqlCluster.crossplane.clusterNamespace -}}
{{- $provider := .Values.postgresqlCluster.crossplane.provider -}}
{{- $tags := dict -}}
{{- if eq $provider "aws" -}}
{{- $awsCluster := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSCluster" $clusterNamespace $clusterName -}}
{{- if $awsCluster -}}
{{- if $awsCluster.spec.additionalTags -}}
{{- range $key, $value := $awsCluster.spec.additionalTags -}}
{{- $_ := set $tags $key $value -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $defaultTags := dict
"app" "grafana-postgresql"
"managed-by" "crossplane"
-}}
{{- $tags = merge $tags $defaultTags -}}
{{- $userTags := .Values.postgresqlCluster.crossplane.tags | default list -}}
{{- range $tag := $userTags -}}
{{- $_ := set $tags (index $tag "key") (index $tag "value") -}}
{{- end -}}
{{- $tags | toYaml -}}
{{- end -}}
62 changes: 62 additions & 0 deletions helm/grafana/templates/cnpg/crossplane/aws/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{{/*
Get AWS Account ID from AWSCluster identity
Supports both AWSClusterRoleIdentity and AWSClusterControllerIdentity
*/}}
{{- define "grafana.crossplane.aws.accountId" -}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we template those variable from values rather than looking them up within the cluster ? This is not a very common pattern across our platform plus it is complex and hard to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that for the others so theoretically yes we could but having to define this value in multiple places does not make sense to me considering this will always run in CAPI MCs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't like the fact that we create a dependency on another object here, whenever this object being lookup change we have to change this. Is there a shared/default config we can use instead of this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be yes, but also, we are dependent on crossplane and a lot of other components anyway.

I'm not sure the oidcProvider is defined in shared configs though @giantswarm/team-phoenix can you let us know?

{{- $clusterName := .Values.postgresqlCluster.crossplane.clusterName -}}
{{- $clusterNamespace := .Values.postgresqlCluster.crossplane.clusterNamespace -}}
{{- $accountId := "" -}}
{{- $awsCluster := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSCluster" $clusterNamespace $clusterName -}}
{{- if $awsCluster -}}
{{- if $awsCluster.spec.identityRef -}}
{{- $identityName := $awsCluster.spec.identityRef.name -}}
{{- $identityKind := $awsCluster.spec.identityRef.kind | default "AWSClusterControllerIdentity" -}}
{{- if eq $identityKind "AWSClusterRoleIdentity" -}}
{{- $identity := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSClusterRoleIdentity" "" $identityName -}}
{{- if $identity -}}
{{- /* Extract account ID from roleARN like arn:aws:iam::758407694730:role/... */ -}}
{{- $roleARN := $identity.spec.roleARN -}}
{{- $parts := regexSplit "::" $roleARN -1 -}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARNs cannot be expected to contain ::. They're made up of values joined by a single colon, and the zero-based index 4 would give you the account ID. See existing code.

There's also no need to implement AWSClusterControllerIdentity lookup at all since we don't support this kind in cluster-aws.

Copy link
Contributor Author

@QuentinBisson QuentinBisson Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then in that case, can we find those values in the configs repos? Because the central place for configuration is shared-configs, yet more and more things are being set in other places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I agree about the lookup but then where can we collect the Cluster Tags to have it applied to the MC crossplane resources?

{{- if gt (len $parts) 1 -}}
{{- $accountId = index (regexSplit ":" (index $parts 1) -1) 0 -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- $identity := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSClusterControllerIdentity" "" $identityName -}}
{{- if $identity -}}
{{- $accountId = $identity.spec.awsAccountID -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $accountId -}}
{{- end -}}

{{/*
Get OIDC Provider URL from cluster
First tries annotation aws.giantswarm.io/irsa-trust-domains, then falls back to identity
*/}}
{{- define "grafana.crossplane.aws.oidcProvider" -}}
{{- $clusterName := .Values.postgresqlCluster.crossplane.clusterName -}}
{{- $clusterNamespace := .Values.postgresqlCluster.crossplane.clusterNamespace -}}
{{- $oidcProvider := "" -}}
{{- $awsCluster := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSCluster" $clusterNamespace $clusterName -}}
{{- if $awsCluster -}}
{{- /* First try to get from annotation (Giant Swarm specific) */ -}}
{{- if $awsCluster.metadata.annotations -}}
{{- $oidcProvider = index $awsCluster.metadata.annotations "aws.giantswarm.io/irsa-trust-domains" | default "" -}}
{{- end -}}
{{- /* If not found in annotation, try identity ref */ -}}
{{- if and (not $oidcProvider) $awsCluster.spec.identityRef -}}
{{- $identityName := $awsCluster.spec.identityRef.name -}}
{{- $identityKind := $awsCluster.spec.identityRef.kind | default "AWSClusterControllerIdentity" -}}
{{- if eq $identityKind "AWSClusterControllerIdentity" -}}
{{- $identity := lookup "infrastructure.cluster.x-k8s.io/v1beta2" "AWSClusterControllerIdentity" "" $identityName -}}
{{- if and $identity $identity.spec.oidc -}}
{{- $oidcProvider = $identity.spec.oidc.issuerURL | trimPrefix "https://" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $oidcProvider -}}
{{- end -}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{- if and .Values.postgresqlCluster.enabled .Values.postgresqlCluster.backup.enabled .Values.postgresqlCluster.crossplane.enabled (include "grafana.crossplane.isAWS" .) .Values.postgresqlCluster.crossplane.aws.enabled }}
{{- $bucketName := .Values.postgresqlCluster.crossplane.aws.bucket.name }}
---
# Configures lifecycle policies for the PostgreSQL backup S3 bucket
# to automatically expire old backups after a configured number of days
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketLifecycleConfiguration
metadata:
name: {{ $bucketName }}
namespace: {{ .Release.Namespace }}
annotations:
crossplane.io/external-name: {{ $bucketName }}
labels:
{{- include "grafana.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
spec:
managementPolicies:
{{- if .Values.postgresqlCluster.crossplane.observeOnly }}
- Observe
{{- else }}
- "*"
{{- end }}
forProvider:
bucketRef:
name: {{ $bucketName }}
region: {{ .Values.postgresqlCluster.crossplane.region }}
rule:
- id: Expiration
status: Enabled
expiration:
- days: {{ .Values.postgresqlCluster.crossplane.aws.bucket.lifecycleDays }}
providerConfigRef:
name: {{ .Values.postgresqlCluster.crossplane.providerConfigRef }}
{{- end }}
51 changes: 51 additions & 0 deletions helm/grafana/templates/cnpg/crossplane/aws/bucket-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{{- if and .Values.postgresqlCluster.enabled .Values.postgresqlCluster.backup.enabled .Values.postgresqlCluster.crossplane.enabled (include "grafana.crossplane.isAWS" .) .Values.postgresqlCluster.crossplane.aws.enabled }}
{{- $bucketName := .Values.postgresqlCluster.crossplane.aws.bucket.name }}
{{- $isChinaRegion := hasPrefix "cn-" .Values.postgresqlCluster.crossplane.region }}
---
# Defines the S3 bucket policy for the PostgreSQL backup bucket
# to enforce secure transport (TLS/SSL) for all access
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketPolicy
metadata:
name: {{ $bucketName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "grafana.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
annotations:
crossplane.io/external-name: {{ $bucketName }}
spec:
managementPolicies:
{{- if .Values.postgresqlCluster.crossplane.observeOnly }}
- Observe
{{- else }}
- "*"
{{- end }}
forProvider:
bucketRef:
name: {{ $bucketName }}
policy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceSSLOnly",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:*"],
"Resource": [
"arn:aws{{- if $isChinaRegion }}-cn{{- end }}:s3:::{{ $bucketName }}",
"arn:aws{{- if $isChinaRegion }}-cn{{- end }}:s3:::{{ $bucketName }}/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
region: {{ .Values.postgresqlCluster.crossplane.region }}
providerConfigRef:
name: {{ .Values.postgresqlCluster.crossplane.providerConfigRef }}
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{- if and .Values.postgresqlCluster.enabled .Values.postgresqlCluster.backup.enabled .Values.postgresqlCluster.crossplane.enabled (include "grafana.crossplane.isAWS" .) .Values.postgresqlCluster.crossplane.aws.enabled }}
{{- $bucketName := .Values.postgresqlCluster.crossplane.aws.bucket.name }}
---
# Blocks all public access to the PostgreSQL backup S3 bucket
# as access is restricted to authorized services via VPC endpoints
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketPublicAccessBlock
metadata:
name: {{ $bucketName }}
namespace: {{ .Release.Namespace }}
annotations:
crossplane.io/external-name: {{ $bucketName }}
labels:
{{- include "grafana.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
spec:
managementPolicies:
{{- if .Values.postgresqlCluster.crossplane.observeOnly }}
- Observe
{{- else }}
- "*"
{{- end }}
forProvider:
bucketRef:
name: {{ $bucketName }}
region: {{ .Values.postgresqlCluster.crossplane.region }}
blockPublicAcls: true
blockPublicPolicy: true
ignorePublicAcls: true
# Prevent public access to the bucket as we use VPC endpoints
restrictPublicBuckets: true
providerConfigRef:
name: {{ .Values.postgresqlCluster.crossplane.providerConfigRef }}
{{- end }}
36 changes: 36 additions & 0 deletions helm/grafana/templates/cnpg/crossplane/aws/bucket.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{{- if and .Values.postgresqlCluster.enabled .Values.postgresqlCluster.backup.enabled .Values.postgresqlCluster.crossplane.enabled (include "grafana.crossplane.isAWS" .) .Values.postgresqlCluster.crossplane.aws.enabled }}
{{- $tags := include "grafana.crossplane.tags" . | fromYaml }}
{{- $bucketName := .Values.postgresqlCluster.crossplane.aws.bucket.name }}
---
# Creates the S3 bucket for storing PostgreSQL backups
# managed by CloudNativePG with barman-cloud
apiVersion: s3.aws.upbound.io/v1beta2
kind: Bucket
metadata:
name: {{ $bucketName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "grafana.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
annotations:
crossplane.io/external-name: {{ $bucketName }}
spec:
managementPolicies:
{{- if .Values.postgresqlCluster.crossplane.observeOnly }}
- Observe
{{- else }}
- "*"
{{- end }}
forProvider:
forceDestroy: false
objectLockEnabled: false
region: {{ .Values.postgresqlCluster.crossplane.region }}
{{- if $tags }}
tags:
{{- range $key, $value := $tags }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
providerConfigRef:
name: {{ .Values.postgresqlCluster.crossplane.providerConfigRef }}
{{- end }}
112 changes: 112 additions & 0 deletions helm/grafana/templates/cnpg/crossplane/aws/iam.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{{- if and .Values.postgresqlCluster.enabled .Values.postgresqlCluster.backup.enabled .Values.postgresqlCluster.crossplane.enabled (include "grafana.crossplane.isAWS" .) .Values.postgresqlCluster.crossplane.aws.enabled .Values.postgresqlCluster.crossplane.aws.iam.enabled }}
{{- $bucketName := .Values.postgresqlCluster.crossplane.aws.bucket.name }}
{{- $namespace := .Release.Namespace }}
{{- $tags := include "grafana.crossplane.tags" . | fromYaml }}
{{- $oidcProvider := include "grafana.crossplane.aws.oidcProvider" . }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use our existing ConfigMap for Crossplane-related resources to read the plural OIDC providers list:

$ kubectl get -n org-myorg cm mycluster-crossplane-config -o yaml
apiVersion: v1
data:
  values: |
    accountID: "150076092023"
    awsCluster:
      securityGroups:
        controlPlane:
          id: sg-04f62403141ca0fce
        node:
          id: sg-0ab5bf55fdf285cbc
      vpcId: vpc-0fda586a4105f1148
    awsPartition: aws
    baseDomain: mycluster.gaws2.gigantic.io
    clusterName: mycluster
    oidcDomain: irsa.mycluster.gaws2.gigantic.io
    oidcDomains:
    - irsa.mycluster.gaws2.gigantic.io
    region: eu-north-1
[...]

In default apps within cluster-aws, such as aws-nth-bundle, we pull the config map in directly (see here). As Theo commented, that would be a more direct way than lookup (which cannot easily be tested in CI).

Ensure you support multiple URLs since migrated Vintage AWS clusters may still have the old and new OIDC provider.

Copy link
Contributor Author

@QuentinBisson QuentinBisson Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that this app is managed by collections so it should not have to mount this config right? Can we defined it in shared configs instead?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you refer to where grafana-app gets deployed, specifically? Is it via capa-app-collection/kustomize/observability-platform-api.yaml or so? I'm trying to figure out how we can add the relevant config.

Copy link
Contributor Author

@QuentinBisson QuentinBisson Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndiDog it is now deployed from https://github.com/giantswarm/management-cluster-bases/blob/convert-to-hr-tempo/bases/collections/shared/base/grafana.yaml

FYI, I'm moving forward here because I will have to change the other apps as well anyway but I have no issues improving it with what you find :)

{{- $isChinaRegion := hasPrefix "cn-" .Values.postgresqlCluster.crossplane.region }}
{{- $serviceAccountName := .Values.postgresqlCluster.name }}
---
# Creates an IAM role for PostgreSQL service accounts to access the backup S3 bucket
# using IRSA (IAM Roles for Service Accounts) with OIDC authentication
apiVersion: iam.aws.upbound.io/v1beta1
kind: Role
metadata:
name: {{ $bucketName }}
namespace: {{ $namespace }}
labels:
{{- include "grafana.labels" . | nindent 4 }}
app.kubernetes.io/component: iam
annotations:
crossplane.io/external-name: {{ $bucketName }}
spec:
managementPolicies:
{{- if .Values.postgresqlCluster.crossplane.observeOnly }}
- Observe
{{- else }}
- "*"
{{- end }}
forProvider:
assumeRolePolicy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws{{- if $isChinaRegion }}-cn{{- end }}:iam::{{ include "grafana.crossplane.aws.accountId" . }}:oidc-provider/{{ $oidcProvider }}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"{{ $oidcProvider }}:sub": "system:serviceaccount:{{ $namespace }}:{{ $serviceAccountName }}",
"{{ $oidcProvider }}:aud": "sts.amazonaws.com{{- if $isChinaRegion }}.cn{{- end }}"
}
}
},
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws{{- if $isChinaRegion }}-cn{{- end }}:iam::{{ include "grafana.crossplane.aws.accountId" . }}:oidc-provider/{{ $oidcProvider }}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"{{ $oidcProvider }}:sub": "system:serviceaccount:{{ $namespace }}:grafana-postgresql-recovery-test",
"{{ $oidcProvider }}:aud": "sts.amazonaws.com{{- if $isChinaRegion }}.cn{{- end }}"
}
}
},
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws{{- if $isChinaRegion }}-cn{{- end }}:iam::{{ include "grafana.crossplane.aws.accountId" . }}:oidc-provider/{{ $oidcProvider }}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"{{ $oidcProvider }}:sub": "system:serviceaccount:{{ $namespace }}:plugin-barman-cloud",
"{{ $oidcProvider }}:aud": "sts.amazonaws.com{{- if $isChinaRegion }}.cn{{- end }}"
}
}
}
]
}
inlinePolicy:
- name: {{ $bucketName }}
policy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws{{- if $isChinaRegion }}-cn{{- end }}:s3:::{{ $bucketName }}",
"arn:aws{{- if $isChinaRegion }}-cn{{- end }}:s3:::{{ $bucketName }}/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetAccessPoint",
"s3:GetAccountPublicAccessBlock",
"s3:ListAccessPoints"
],
"Resource": "*"
}
]
}
{{- if $tags }}
tags:
{{- range $key, $value := $tags }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
providerConfigRef:
name: {{ .Values.postgresqlCluster.crossplane.providerConfigRef }}
{{- end }}
Loading