From f4ab1f05f71d4c34e574067758ef01ac63bd420a Mon Sep 17 00:00:00 2001 From: Imtiaz Uddin Date: Thu, 20 Aug 2026 14:52:41 +0600 Subject: [PATCH 1/3] Create a service account for the billing chart The aggregator statefulset, summary cronjob and processor resolved their service account through a helper that fell back to `default`, so the pods ran with no identity of their own. Referencing the ace chart's account by name is not an option either: it is `ace.fullname`, so a release named ace-platform owns `ace-platform`, not `ace`. Add templates/rbac/serviceaccount.yaml and follow the ace chart's helper shape, deriving the name from `billing.fullname` when `serviceAccount.create` is set. `serviceAccount.annotations` carries the cloud identity binding (e.g. iam.gke.io/gcp-service-account), the same way the ace chart passes `global.serviceAccount.annotations`. Setting `serviceAccount.create` to false keeps the old behaviour, and combining it with `serviceAccount.name` points the pods at an existing account such as the one the ace release creates. Signed-off-by: Imtiaz Uddin --- apis/installer/v1alpha1/ace_billing_types.go | 4 ++-- charts/billing/README.md | 4 +++- charts/billing/templates/_helpers.tpl | 4 ++++ charts/billing/templates/rbac/serviceaccount.yaml | 13 +++++++++++++ charts/billing/values.openapiv3_schema.yaml | 8 +++++++- charts/billing/values.yaml | 6 ++++++ 6 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 charts/billing/templates/rbac/serviceaccount.yaml diff --git a/apis/installer/v1alpha1/ace_billing_types.go b/apis/installer/v1alpha1/ace_billing_types.go index 130f2ff0a..7d34b0d09 100644 --- a/apis/installer/v1alpha1/ace_billing_types.go +++ b/apis/installer/v1alpha1/ace_billing_types.go @@ -54,8 +54,8 @@ type BillingSpec struct { //+optional NameOverride string `json:"nameOverride"` //+optional - FullnameOverride string `json:"fullnameOverride"` - ServiceAccount LocalObjectReference `json:"serviceAccount"` + FullnameOverride string `json:"fullnameOverride"` + ServiceAccount ServiceAccountSpec `json:"serviceAccount"` //+optional PodAnnotations map[string]string `json:"podAnnotations"` //+optional diff --git a/charts/billing/README.md b/charts/billing/README.md index 885150c2e..32f223f1a 100644 --- a/charts/billing/README.md +++ b/charts/billing/README.md @@ -56,7 +56,9 @@ The following table lists the configurable parameters of the `billing` chart and | imagePullSecrets | | [] | | nameOverride | | "" | | fullnameOverride | | "" | -| serviceAccount.name | | "" | +| serviceAccount.create | Specifies whether a service account should be created | true | +| serviceAccount.annotations | Annotations to add to the service account | {} | +| serviceAccount.name | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | "" | | podAnnotations | | {} | | podSecurityContext.fsGroup | | 65534 | | securityContext | Security options this container should run with | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsNonRoot":true,"runAsUser":65534,"seccompProfile":{"type":"RuntimeDefault"}} | diff --git a/charts/billing/templates/_helpers.tpl b/charts/billing/templates/_helpers.tpl index 4a3fb1ab5..deccdc769 100644 --- a/charts/billing/templates/_helpers.tpl +++ b/charts/billing/templates/_helpers.tpl @@ -54,8 +54,12 @@ app.kubernetes.io/instance: {{ .Release.Name }} Create the name of the service account to use */}} {{- define "appscode.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "billing.fullname" .) .Values.serviceAccount.name }} +{{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} +{{- end }} {{/* Returns the registry used for image docker image diff --git a/charts/billing/templates/rbac/serviceaccount.yaml b/charts/billing/templates/rbac/serviceaccount.yaml new file mode 100644 index 000000000..6734bd7e2 --- /dev/null +++ b/charts/billing/templates/rbac/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "appscode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "billing.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/billing/values.openapiv3_schema.yaml b/charts/billing/values.openapiv3_schema.yaml index a69560cb0..ace58f8a1 100644 --- a/charts/billing/values.openapiv3_schema.yaml +++ b/charts/billing/values.openapiv3_schema.yaml @@ -762,10 +762,16 @@ properties: type: object serviceAccount: properties: + annotations: + additionalProperties: + type: string + type: object + create: + type: boolean name: type: string required: - - name + - create type: object settings: properties: diff --git a/charts/billing/values.yaml b/charts/billing/values.yaml index 916058df1..3eb6807dc 100644 --- a/charts/billing/values.yaml +++ b/charts/billing/values.yaml @@ -23,6 +23,12 @@ nameOverride: "" fullnameOverride: "" serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} From 08e686478b15102a25f8df4a69a5dc6736714d82 Mon Sep 17 00:00:00 2001 From: Imtiaz Uddin Date: Thu, 20 Aug 2026 15:04:28 +0600 Subject: [PATCH 2/3] Bind the billing service account to the license roles The aggregator dies during startup because the license check reads the kube-system namespace to derive the cluster id: [F] Failed to run app with [/b3 aggregator --interval=30s]: license status unknown, reason: namespaces "kube-system" is forbidden: User "system:serviceaccount:ace:default" cannot get resource "namespaces" platform-api solves this with the appscode:license-checker and appscode:license-reader cluster roles bound to its own service account, so ship the same pair from the billing chart's rbac folder. Both roles are created as pre-install/pre-upgrade hooks under their shared fixed names, exactly as platform-api and accounts-ui do, so releases do not fight over ownership. The ace chart's own rbac.yaml is not reused here: its rules cover trickster, CAPI clusters, vcluster and flux bootstrap data, none of which the billing components touch. Signed-off-by: Imtiaz Uddin --- charts/billing/templates/rbac/rbac.yaml | 154 ++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 charts/billing/templates/rbac/rbac.yaml diff --git a/charts/billing/templates/rbac/rbac.yaml b/charts/billing/templates/rbac/rbac.yaml new file mode 100644 index 000000000..8170b945a --- /dev/null +++ b/charts/billing/templates/rbac/rbac.yaml @@ -0,0 +1,154 @@ +{{- /* +appscode:license-checker and appscode:license-reader are shared with the ace +chart (via platform-api), which templates them identically. Both charts declare +them as pre-install/pre-upgrade hooks with hook-delete-policy +before-hook-creation, so a hook run deletes and recreates whatever is already +there - the chart reconciled last would win. Create them here only when they are +absent so the ace chart stays authoritative and billing never overwrites them. +Permissions billing needs on top of the shared set belong in the billing-owned +ClusterRole at the bottom of this file, which is never contested. +*/}} +{{- if not (lookup "rbac.authorization.k8s.io/v1" "ClusterRole" "" "appscode:license-checker") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: appscode:license-checker + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +rules: +# Get cluster id +- apiGroups: + - "" + resources: + - namespaces + verbs: ["get"] +# Issue license +- apiGroups: + - proxyserver.licenses.appscode.com + resources: + - licenserequests + verbs: ["create"] +# Detect workload/owner of operator pod +- apiGroups: + - "" + resources: + - pods + verbs: ["get"] +- apiGroups: + - apps + resources: + - deployments + - replicasets + verbs: ["get"] +# Write events in case of license verification failure +- apiGroups: + - "" + resources: + - events + verbs: ["get", "list", "create", "patch"] +{{- end }} + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "billing.fullname" . }}-license-checker + labels: + {{- include "billing.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: appscode:license-checker +subjects: +- kind: ServiceAccount + name: {{ include "appscode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + +--- + +{{- if not (lookup "rbac.authorization.k8s.io/v1" "ClusterRole" "" "appscode:license-reader") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: appscode:license-reader + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +rules: +# Detect license server endpoint for kubedb addons +- apiGroups: + - apiregistration.k8s.io + resources: + - apiservices + verbs: ["get"] +- nonResourceURLs: + - /appscode/license + verbs: ["get"] +{{- end }} + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "billing.fullname" . }}-license-reader + labels: + {{- include "billing.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: appscode:license-reader +subjects: +- kind: ServiceAccount + name: {{ include "appscode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "billing.fullname" . }} + labels: + {{- include "billing.labels" . | nindent 4 }} +rules: +# Read the cluster metadata the license enforcer and the site info publisher +# need. Kept here rather than in appscode:license-checker because that role is +# shared with the ace chart and would lose this rule the next time the ace +# chart reconciles it. +- apiGroups: + - "" + resources: + - configmaps + verbs: ["get"] + resourceNames: ["ace-info"] +# for site info +- apiGroups: + - "" + resources: + - nodes + verbs: ["get", "list", "watch"] +- apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: ["get", "list", "watch"] + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "billing.fullname" . }} + labels: + {{- include "billing.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "billing.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "appscode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} From 02b042ea83412ab0a280268edd3934d029bf3e41 Mon Sep 17 00:00:00 2001 From: Imtiaz Uddin Date: Thu, 20 Aug 2026 15:35:24 +0600 Subject: [PATCH 3/3] make gen fmt Signed-off-by: Imtiaz Uddin --- apis/installer/v1alpha1/zz_generated.deepcopy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/installer/v1alpha1/zz_generated.deepcopy.go b/apis/installer/v1alpha1/zz_generated.deepcopy.go index 59121fd34..51404cb9e 100644 --- a/apis/installer/v1alpha1/zz_generated.deepcopy.go +++ b/apis/installer/v1alpha1/zz_generated.deepcopy.go @@ -2629,7 +2629,7 @@ func (in *BillingSpec) DeepCopyInto(out *BillingSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - out.ServiceAccount = in.ServiceAccount + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) if in.PodAnnotations != nil { in, out := &in.PodAnnotations, &out.PodAnnotations *out = make(map[string]string, len(*in))