From 0ffe030b1cf1c94b0bb903c50127b107db0d5c1c Mon Sep 17 00:00:00 2001 From: Kanwar Ujjaval Singh <4216199+kanwarujjaval@users.noreply.github.com> Date: Thu, 7 May 2026 14:09:27 +0530 Subject: [PATCH 1/2] add ui canary charts --- argocd/applicationsets/06-canary-web-ui.yaml | 74 ++++++++++++++++++ charts/countly-web-ui-canary/.helmignore | 9 +++ charts/countly-web-ui-canary/Chart.yaml | 9 +++ .../templates/_helpers.tpl | 58 ++++++++++++++ .../templates/deployment.yaml | 75 +++++++++++++++++++ .../templates/ingress.yaml | 59 +++++++++++++++ .../templates/networkpolicy.yaml | 23 ++++++ .../templates/service.yaml | 16 ++++ charts/countly-web-ui-canary/values.yaml | 44 +++++++++++ 9 files changed, 367 insertions(+) create mode 100644 argocd/applicationsets/06-canary-web-ui.yaml create mode 100644 charts/countly-web-ui-canary/.helmignore create mode 100644 charts/countly-web-ui-canary/Chart.yaml create mode 100644 charts/countly-web-ui-canary/templates/_helpers.tpl create mode 100644 charts/countly-web-ui-canary/templates/deployment.yaml create mode 100644 charts/countly-web-ui-canary/templates/ingress.yaml create mode 100644 charts/countly-web-ui-canary/templates/networkpolicy.yaml create mode 100644 charts/countly-web-ui-canary/templates/service.yaml create mode 100644 charts/countly-web-ui-canary/values.yaml diff --git a/argocd/applicationsets/06-canary-web-ui.yaml b/argocd/applicationsets/06-canary-web-ui.yaml new file mode 100644 index 0000000..34e5d09 --- /dev/null +++ b/argocd/applicationsets/06-canary-web-ui.yaml @@ -0,0 +1,74 @@ +# ApplicationSet that creates one ArgoCD Application per countly-web-ui canary. +# +# Layout split: +# - chart: helm/charts/countly-web-ui-canary/ (this repo) +# - per-canary values: countly-deployment/argocd/canaries//values.yaml +# - cert-manager bootstrap App + ClusterIssuer: countly-deployment/argocd/{applications,bootstrap}/ +# +# This file lives in helm/ because the root `countly-bootstrap` Application +# (helm/argocd/root-application.yaml) auto-syncs everything in helm/argocd/. +# Putting it here means the ApplicationSet is git-discovered without any +# ArgoCD UI step. +# +# Generator: scans countly-deployment/argocd/canaries/*/values.yaml and +# produces one Application per file. Pipeline B (countly-web-ui's +# canary-build-deploy.yml) writes/deletes those files. +# +# Sources (multi-source): the chart comes from helm.git, the per-canary +# values file comes from countly-deployment.git via the $values alias. +# +# Destination: hardcoded to v2-new (https://34.62.8.139). No matrix or +# cluster fan-out — canaries can never land on other clusters. +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: canary-web-ui + namespace: argocd + labels: + app.kubernetes.io/part-of: countly + app.kubernetes.io/component: canary-web-ui +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://github.com/Countly/countly-deployment.git + revision: main + files: + - path: argocd/canaries/*/values.yaml + template: + metadata: + name: 'canary-ui-{{ .path.basename }}' + labels: + app.kubernetes.io/part-of: countly + app.kubernetes.io/component: canary-web-ui + countly.io/canary: '{{ .path.basename }}' + spec: + project: countly-customers + sources: + - repoURL: https://github.com/Countly/helm.git + targetRevision: main + path: charts/countly-web-ui-canary + helm: + releaseName: 'canary-ui-{{ .path.basename }}' + valueFiles: + - '$values/argocd/canaries/{{ .path.basename }}/values.yaml' + - repoURL: https://github.com/Countly/countly-deployment.git + targetRevision: main + ref: values + destination: + server: https://34.62.8.139 + namespace: countly + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=false + - ApplyOutOfSyncOnly=true + retry: + limit: 3 + backoff: + duration: 10s + factor: 2 + maxDuration: 2m diff --git a/charts/countly-web-ui-canary/.helmignore b/charts/countly-web-ui-canary/.helmignore new file mode 100644 index 0000000..a9e5b91 --- /dev/null +++ b/charts/countly-web-ui-canary/.helmignore @@ -0,0 +1,9 @@ +.DS_Store +.git/ +.gitignore +*.swp +*.bak +*.tmp +*.orig +.vscode/ +.idea/ diff --git a/charts/countly-web-ui-canary/Chart.yaml b/charts/countly-web-ui-canary/Chart.yaml new file mode 100644 index 0000000..e213380 --- /dev/null +++ b/charts/countly-web-ui-canary/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: countly-web-ui-canary +description: Per-branch preview deployment of the countly-web-ui SPA against the stable v2-new backend. +type: application +version: 0.1.0 +appVersion: "1" +maintainers: + - name: Countly +icon: https://count.ly/favicon.ico diff --git a/charts/countly-web-ui-canary/templates/_helpers.tpl b/charts/countly-web-ui-canary/templates/_helpers.tpl new file mode 100644 index 0000000..426f42c --- /dev/null +++ b/charts/countly-web-ui-canary/templates/_helpers.tpl @@ -0,0 +1,58 @@ +{{/* Required values guard */}} +{{- define "canary.slug" -}} +{{- required "values.slug is required" .Values.slug -}} +{{- end -}} + +{{- define "canary.hostname" -}} +{{- required "values.hostname is required" .Values.hostname -}} +{{- end -}} + +{{/* Resource name shared by Deployment, Service, Ingress, NetworkPolicy. + k8s name length cap is 63; truncate if a long slug is passed. */}} +{{- define "canary.fullname" -}} +{{- printf "canary-ui-%s" (include "canary.slug" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* Standard label set */}} +{{- define "canary.labels" -}} +app.kubernetes.io/name: countly-web-ui-canary +app.kubernetes.io/instance: {{ include "canary.fullname" . }} +app.kubernetes.io/component: frontend +app.kubernetes.io/part-of: countly +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +countly.io/canary: {{ include "canary.slug" . | quote }} +countly.io/branch: {{ .Values.branch | quote }} +countly.io/sha: {{ .Values.sha | quote }} +{{- end -}} + +{{/* Selector labels: stable subset used by Service/Deployment selectors */}} +{{- define "canary.selectorLabels" -}} +app.kubernetes.io/name: countly-web-ui-canary +app.kubernetes.io/instance: {{ include "canary.fullname" . }} +countly.io/canary: {{ include "canary.slug" . | quote }} +{{- end -}} + +{{/* Image reference: prefer digest over tag */}} +{{- define "canary.image" -}} +{{- $repo := required "values.image.repository is required" .Values.image.repository -}} +{{- if .Values.image.digest -}} +{{- printf "%s@%s" $repo .Values.image.digest -}} +{{- else if .Values.image.tag -}} +{{- printf "%s:%s" $repo .Values.image.tag -}} +{{- else -}} +{{- fail "either image.digest or image.tag must be set" -}} +{{- end -}} +{{- end -}} + +{{/* Default ingress annotations merged with per-canary overrides */}} +{{- define "canary.ingressAnnotations" -}} +nginx.org/client-max-body-size: "50m" +nginx.org/proxy-buffering: "True" +nginx.org/proxy-read-timeout: "120s" +nginx.org/proxy-send-timeout: "120s" +nginx.org/keepalive: "256" +{{- with .Values.ingress.annotations }} +{{ toYaml . }} +{{- end }} +{{- end -}} diff --git a/charts/countly-web-ui-canary/templates/deployment.yaml b/charts/countly-web-ui-canary/templates/deployment.yaml new file mode 100644 index 0000000..e2c5abc --- /dev/null +++ b/charts/countly-web-ui-canary/templates/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "canary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "canary.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicas }} + revisionHistoryLimit: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + {{- include "canary.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "canary.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + securityContext: + runAsNonRoot: true + runAsUser: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + containers: + - name: nginx + image: {{ include "canary.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + ports: + - name: http + containerPort: 8080 + protocol: TCP + startupProbe: + httpGet: { path: /healthz, port: http } + periodSeconds: 5 + failureThreshold: 12 + timeoutSeconds: 3 + readinessProbe: + httpGet: { path: /healthz, port: http } + periodSeconds: 10 + timeoutSeconds: 3 + livenessProbe: + httpGet: { path: /healthz, port: http } + periodSeconds: 30 + timeoutSeconds: 3 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: cache + mountPath: /var/cache/nginx + - name: run + mountPath: /var/run + - name: tmp + mountPath: /tmp + volumes: + - name: cache + emptyDir: {} + - name: run + emptyDir: {} + - name: tmp + emptyDir: {} diff --git a/charts/countly-web-ui-canary/templates/ingress.yaml b/charts/countly-web-ui-canary/templates/ingress.yaml new file mode 100644 index 0000000..fa85976 --- /dev/null +++ b/charts/countly-web-ui-canary/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- $fullname := include "canary.fullname" . -}} +{{- $stable := .Values.backend.release -}} +{{- $apiPort := .Values.backend.ports.api | int -}} +{{- $ingestorPort := .Values.backend.ports.ingestor | int -}} +{{- $jobserverPort := .Values.backend.ports.jobserver | int -}} +{{- $tlsSecret := printf "%s-tls" $fullname -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "canary.labels" . | nindent 4 }} + annotations: + cert-manager.io/cluster-issuer: {{ required "ingress.tls.clusterIssuer is required" .Values.ingress.tls.clusterIssuer | quote }} + {{- include "canary.ingressAnnotations" . | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.className }} + tls: + - hosts: + - {{ include "canary.hostname" . | quote }} + secretName: {{ $tlsSecret | quote }} + rules: + - host: {{ include "canary.hostname" . | quote }} + http: + paths: + - path: /i/bulk + pathType: Exact + backend: { service: { name: {{ printf "%s-ingestor" $stable }}, port: { number: {{ $ingestorPort }} } } } + - path: /i/feedback/input + pathType: Exact + backend: { service: { name: {{ printf "%s-ingestor" $stable }}, port: { number: {{ $ingestorPort }} } } } + - path: /i/feedback/inputs + pathType: Exact + backend: { service: { name: {{ printf "%s-ingestor" $stable }}, port: { number: {{ $ingestorPort }} } } } + - path: /i + pathType: Exact + backend: { service: { name: {{ printf "%s-ingestor" $stable }}, port: { number: {{ $ingestorPort }} } } } + - path: /i/ + pathType: Prefix + backend: { service: { name: {{ printf "%s-api" $stable }}, port: { number: {{ $apiPort }} } } } + - path: /o + pathType: Exact + backend: { service: { name: {{ printf "%s-api" $stable }}, port: { number: {{ $apiPort }} } } } + - path: /o/ + pathType: Prefix + backend: { service: { name: {{ printf "%s-api" $stable }}, port: { number: {{ $apiPort }} } } } + - path: /api + pathType: Prefix + backend: { service: { name: {{ printf "%s-api" $stable }}, port: { number: {{ $apiPort }} } } } + - path: /v2 + pathType: Prefix + backend: { service: { name: {{ printf "%s-api" $stable }}, port: { number: {{ $apiPort }} } } } + - path: /jobs + pathType: Prefix + backend: { service: { name: {{ printf "%s-jobserver" $stable }}, port: { number: {{ $jobserverPort }} } } } + - path: / + pathType: Prefix + backend: { service: { name: {{ $fullname }}, port: { number: 80 } } } diff --git a/charts/countly-web-ui-canary/templates/networkpolicy.yaml b/charts/countly-web-ui-canary/templates/networkpolicy.yaml new file mode 100644 index 0000000..81232c4 --- /dev/null +++ b/charts/countly-web-ui-canary/templates/networkpolicy.yaml @@ -0,0 +1,23 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "canary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "canary.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "canary.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + {{- toYaml .Values.networkPolicy.ingressNamespaceSelector | nindent 14 }} + ports: + - port: 8080 + protocol: TCP +{{- end }} diff --git a/charts/countly-web-ui-canary/templates/service.yaml b/charts/countly-web-ui-canary/templates/service.yaml new file mode 100644 index 0000000..72587d9 --- /dev/null +++ b/charts/countly-web-ui-canary/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "canary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "canary.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + {{- include "canary.selectorLabels" . | nindent 4 }} diff --git a/charts/countly-web-ui-canary/values.yaml b/charts/countly-web-ui-canary/values.yaml new file mode 100644 index 0000000..4ab1f02 --- /dev/null +++ b/charts/countly-web-ui-canary/values.yaml @@ -0,0 +1,44 @@ +# Per-canary identity. Pipeline B writes these. +slug: "" # required, e.g. "canary-foo-9f0e123" +hostname: "" # required, e.g. "canary-foo-9f0e123-ui.v2.count.ly" +branch: "" # informational; populates labels +sha: "" # informational; populates labels + +replicas: 1 + +image: + repository: us-docker.pkg.dev/countly-01/internal/countly-web-ui + digest: "" # preferred — Pipeline B always sets this + tag: "" # fallback; ignored if digest is set + pullPolicy: IfNotPresent + +# Stable backend services in this same namespace, reused by ingress routing. +backend: + release: countly + ports: + api: 3001 + ingestor: 3010 + jobserver: 3020 + +ingress: + className: nginx + tls: + # cert-manager auto-provisions a per-canary Let's Encrypt cert via HTTP-01. + # Each canary gets its own Secret named canary-ui--tls (created by cert-manager + # on first sync; ~30s ACME delay). The ClusterIssuer must be installed once on v2-new + # (see countly-deployment/argocd/bootstrap/letsencrypt-clusterissuer.yaml). + clusterIssuer: letsencrypt-prod-http01 + annotations: {} + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + +networkPolicy: + enabled: true + ingressNamespaceSelector: + kubernetes.io/metadata.name: ingress-nginx From 11c7dc751b8bcd6c81115a73c57a39ed42c4643c Mon Sep 17 00:00:00 2001 From: Kanwar Ujjaval Singh <4216199+kanwarujjaval@users.noreply.github.com> Date: Thu, 7 May 2026 14:17:20 +0530 Subject: [PATCH 2/2] fix chart lint --- .github/workflows/validate-charts.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/validate-charts.yml b/.github/workflows/validate-charts.yml index 0ae7d00..1cbb2df 100644 --- a/.github/workflows/validate-charts.yml +++ b/.github/workflows/validate-charts.yml @@ -123,6 +123,13 @@ jobs: --set backingServices.clickhouse.password=test \ > /dev/null || exit_code=1 ;; + countly-web-ui-canary) + helm template test-release "${chart}" \ + --set slug=test \ + --set hostname=test.v2.count.ly \ + --set image.tag=test \ + > /dev/null || exit_code=1 + ;; *) helm template test-release "${chart}" > /dev/null || exit_code=1 ;;