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
7 changes: 7 additions & 0 deletions .github/workflows/validate-charts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
74 changes: 74 additions & 0 deletions argocd/applicationsets/06-canary-web-ui.yaml
Original file line number Diff line number Diff line change
@@ -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/<slug>/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
9 changes: 9 additions & 0 deletions charts/countly-web-ui-canary/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
.git/
.gitignore
*.swp
*.bak
*.tmp
*.orig
.vscode/
.idea/
9 changes: 9 additions & 0 deletions charts/countly-web-ui-canary/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions charts/countly-web-ui-canary/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -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 -}}
75 changes: 75 additions & 0 deletions charts/countly-web-ui-canary/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -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: {}
59 changes: 59 additions & 0 deletions charts/countly-web-ui-canary/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -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 } } }
23 changes: 23 additions & 0 deletions charts/countly-web-ui-canary/templates/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
16 changes: 16 additions & 0 deletions charts/countly-web-ui-canary/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
44 changes: 44 additions & 0 deletions charts/countly-web-ui-canary/values.yaml
Original file line number Diff line number Diff line change
@@ -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-<slug>-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
Loading