diff --git a/.github/workflows/lint-jobs.yml b/.github/workflows/lint-jobs.yml index 94858b08..3388fca1 100644 --- a/.github/workflows/lint-jobs.yml +++ b/.github/workflows/lint-jobs.yml @@ -22,6 +22,9 @@ jobs: - uses: actions/checkout@v4 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: "-e SC1091" # ignores using .env for source as issue + markdown-lint: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 62dc0faa..d9f26242 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,7 @@ pathservice.pid /playwright-report/ /blob-report/ /playwright/.cache/ - playwright/.auth +# dont track secrets in git +deploy/k8s/overlays/kind/umami/umami-secret.yaml +deploy/k8s/overlays/openshift/umami/umami-secret.yaml diff --git a/Makefile b/Makefile index d812a972..e45d387f 100644 --- a/Makefile +++ b/Makefile @@ -16,12 +16,17 @@ else PIPE_DEV_NULL= endif +#add an alias between kubectl and oc +OC := $(shell command -v oc 2>/dev/null || echo kubectl) ILAB_KUBE_CONTEXT?=kind-instructlab-ui ILAB_KUBE_NAMESPACE?=instructlab ILAB_KUBE_CLUSTER_NAME?=instructlab-ui CONTAINER_ENGINE?=docker DEVCONTAINER_BINARY_EXISTS ?= $(shell command -v devcontainer) TAG=$(shell git rev-parse HEAD) +UMAMI_KUBE_NAMESPACE?=umami +SEALED_SECRETS_CONTROLLER_NAMESPACE=kube-system +SEALED_SECRETS_CONTROLLER_NAME=sealed-secrets-controller ##@ Development - Helper commands for development .PHONY: md-lint md-lint: ## Lint markdown files @@ -89,7 +94,6 @@ start-dev-podman: ## Start UI development stack in podman echo "Please create a .env file in the root of the project." ; \ exit 1 ; \ fi - $(CMD_PREFIX) yes | cp -rf .env ./deploy/compose/.env $(CMD_PREFIX) podman-compose -f ./deploy/compose/ui-compose.yml up -d $(CMD_PREFIX) echo "Development environment started." @@ -111,9 +115,37 @@ check-kubectl: exit 1 ; \ fi +.PHONY: check-kubeseal +check-kubeseal: + $(CMD_PREFIX) if [ -z "$(shell which kubeseal)" ]; then \ + echo "Please install kubeseal" ; \ + echo "https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#kubeseal" ; \ + exit 1 ; \ + fi + +.PHONY: check-sealed-secrets-controller +check-sealed-secrets-controller: + $(CMD_PREFIX) kubectl get deployment ${SEALED_SECRETS_CONTROLLER_NAME} -n ${SEALED_SECRETS_CONTROLLER_NAMESPACE} > /dev/null 2>&1 || { \ + echo "Error: Could not find the Sealed Secrets controller deployment named '${SEALED_SECRETS_CONTROLLER_NAME}' in namespace '${SEALED_SECRETS_CONTROLLER_NAMESPACE}'."; \ + echo "Please update SEALED_SECRETS_CONTROLLER_NAME and SEALED_SECRETS_CONTROLLER_NAMESPACE at the top of the Makefile"; \ + echo "to match your deployment, or see https://github.com/bitnami-labs/sealed-secrets#controller for information on installing it."; \ + exit 1; \ + } + +.PHONY: check-yq +check-yq: + $(CMD_PREFIX) if ! command -v yq >/dev/null 2>&1; then \ + echo "Error: 'yq' is not installed."; \ + echo "Please visit https://github.com/mikefarah/yq#install for installation instructions."; \ + exit 1; \ + fi + .PHONY: load-images load-images: ## Load images onto Kind cluster + $(CMD_PREFIX) docker pull ghcr.io/instructlab/ui/ui:main $(CMD_PREFIX) kind load --name $(ILAB_KUBE_CLUSTER_NAME) docker-image ghcr.io/instructlab/ui/ui:main + $(CMD_PREFIX) docker pull postgres:15-alpine + $(CMD_PREFIX) kind load --name $(ILAB_KUBE_CLUSTER_NAME) docker-image postgres:15-alpine .PHONY: stop-dev-kind stop-dev-kind: check-kind ## Stop the Kind cluster to destroy the development environment @@ -152,8 +184,30 @@ undeploy: ## Undeploy the InstructLab UI stack from a kubernetes cluster fi $(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) delete namespace $(ILAB_KUBE_NAMESPACE) +.PHONY: deploy-umami-kind +deploy-umami-kind: wait-for-readiness load-images + $(CMD_PREFIX) if [ ! -f .env ]; then \ + echo "Please create a .env file in the root of the project." ; \ + exit 1 ; \ + fi + $(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) create namespace $(UMAMI_KUBE_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + $(CMD_PREFIX) bash -c "source .env && \ + deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh KIND $(UMAMI_KUBE_NAMESPACE)" + $(CMD_PREFIX) kubectl create -f ./deploy/k8s/overlays/kind/umami/umami-secret.yaml + $(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) apply -k ./deploy/k8s/overlays/kind/umami + $(CMD_PREFIX) echo "Waiting for Umami Deployment (pods: postgresql and umami) ..." + $(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) wait --for=condition=Ready pods -n $(UMAMI_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=umami --timeout=15m + $(CMD_PREFIX) umami_ingress=$$(kubectl get ingress umami-ingress -n umami -o jsonpath='{.spec.rules[*].host}') ; \ + echo "Umami ingress deployed to: $$umami_ingress" + +.PHONY: undeploy-umami-kind +undeploy-umami-kind: + -$(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) scale --replicas=0 deployment/umami -n $(UMAMI_KUBE_NAMESPACE) + -$(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) delete -f ./deploy/k8s/overlays/kind/umami/umami-secret.yaml + -$(CMD_PREFIX) kubectl --context=$(ILAB_KUBE_CONTEXT) delete -k ./deploy/k8s/overlays/kind/umami + .PHONY: start-dev-kind ## Run the development environment on Kind cluster -start-dev-kind: setup-kind deploy ## Setup a Kind cluster and deploy InstructLab UI on it +start-dev-kind: setup-kind load-images deploy ## Setup a Kind cluster and deploy InstructLab UI on it ##@ OpenShift - UI prod and qa deployment on OpenShift .PHONY: deploy-qa-openshift @@ -162,53 +216,97 @@ deploy-qa-openshift: ## Deploy QA stack of the InstructLab UI on OpenShift echo "Please create a .env file in the root of the project." ; \ exit 1 ; \ fi - $(CMD_PREFIX) yes | cp -rf .env ./deploy/k8s/overlays/openshift/qa/.env - $(CMD_PREFIX) oc apply -k ./deploy/k8s/overlays/openshift/qa - $(CMD_PREFIX) oc wait --for=condition=Ready pods -n $(ILAB_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=ui --timeout=15m + $(CMD_PREFIX) $(OC) apply -k ./deploy/k8s/overlays/openshift/qa + $(CMD_PREFIX) $(OC) wait --for=condition=Ready pods -n $(ILAB_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=ui --timeout=15m .PHONY: redeploy-qa-openshift redeploy-qa-openshift: ## Redeploy QA stack of the InstructLab UI on OpenShift - $(CMD_PREFIX) oc -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/ui - $(CMD_PREFIX) oc -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/pathservice - + $(CMD_PREFIX) $(OC) -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/ui + $(CMD_PREFIX) $(OC) -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/pathservice .PHONY: undeploy-qa-openshift undeploy-qa-openshift: ## Undeploy QA stack of the InstructLab UI on OpenShift - $(CMD_PREFIX) oc delete -k ./deploy/k8s/overlays/openshift/qa + $(CMD_PREFIX) $(OC) delete -k ./deploy/k8s/overlays/openshift/qa $(CMD_PREFIX) if [ -f ./deploy/k8s/overlays/openshift/qa/.env ]; then \ rm ./deploy/k8s/overlays/openshift/qa/.env ; \ fi +.PHONY: deploy-umami-qa-openshift +deploy-umami-qa-openshift: + $(CMD_PREFIX) if [ ! -f .env ]; then \ + echo "Please create a .env file in the root of the project." ; \ + exit 1 ; \ + fi + $(CMD_PREFIX) $(OC) create namespace $(UMAMI_KUBE_NAMESPACE) --dry-run=client -o yaml | $(OC) apply -f - + $(CMD_PREFIX) source .env && \ + deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh OPENSHIFT $(UMAMI_KUBE_NAMESPACE) + $(CMD_PREFIX) $(OC) apply -f ./deploy/k8s/overlays/openshift/umami/umami-secret.yaml + $(CMD_PREFIX) $(OC) apply -k ./deploy/k8s/overlays/openshift/umami + $(CMD_PREFIX) echo "Waiting for Umami Deployment (pods: postgresql and umami) ..." + $(CMD_PREFIX) $(OC) wait --for=condition=Ready pods -n $(UMAMI_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=umami --timeout=15m + $(CMD_PREFIX) umami_route=$$($(OC) get route umami -n $(UMAMI_KUBE_NAMESPACE) | tail -n 1 | awk '{print $$2}') ; \ + echo "Umami route deployed to: $$umami_route" + +.PHONY: undeploy-umami-qa-openshift +undeploy-umami-qa-openshift: + -$(CMD_PREFIX) $(OC) scale --replicas=0 deployment/umami -n $(UMAMI_KUBE_NAMESPACE) + -$(CMD_PREFIX) $(OC) delete -f ./deploy/k8s/overlays/openshift/umami/umami-secret.yaml + -$(CMD_PREFIX) $(OC) delete -k ./deploy/k8s/overlays/openshift/umami + .PHONY: deploy-prod-openshift deploy-prod-openshift: ## Deploy production stack of the InstructLab UI on OpenShift $(CMD_PREFIX) if [ ! -f .env ]; then \ echo "Please create a .env file in the root of the project." ; \ exit 1 ; \ fi - $(CMD_PREFIX) yes | cp -rf .env ./deploy/k8s/overlays/openshift/prod/.env - $(CMD_PREFIX) oc apply -k ./deploy/k8s/overlays/openshift/prod - $(CMD_PREFIX) oc wait --for=condition=Ready pods -n $(ILAB_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=ui --timeout=15m + $(CMD_PREFIX) $(OC) apply -k ./deploy/k8s/overlays/openshift/prod + $(CMD_PREFIX) $(OC) wait --for=condition=Ready pods -n $(ILAB_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=ui --timeout=15m .PHONY: redeploy-prod-openshift redeploy-prod-openshift: ## Redeploy production stack of the InstructLab UI on OpenShift - $(CMD_PREFIX) oc -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/ui - $(CMD_PREFIX) oc -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/pathservice - + $(CMD_PREFIX) $(OC) -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/ui + $(CMD_PREFIX) $(OC) -n $(ILAB_KUBE_NAMESPACE) rollout restart deploy/pathservice .PHONY: undeploy-prod-openshift undeploy-prod-openshift: ## Undeploy production stack of the InstructLab UI on OpenShift - $(CMD_PREFIX) oc delete -k ./deploy/k8s/overlays/openshift/prod + $(CMD_PREFIX) $(OC) delete -k ./deploy/k8s/overlays/openshift/prod $(CMD_PREFIX) if [ -f ./deploy/k8s/overlays/openshift/prod/.env ]; then \ rm ./deploy/k8s/overlays/openshift/prod/.env ; \ fi +.PHONY: deploy-umami-prod-openshift +deploy-umami-prod-openshift: check-kubeseal check-sealed-secrets-controller + $(CMD_PREFIX) if [ ! -f .env ]; then \ + echo "Please create a .env file in the root of the project." ; \ + exit 1 ; \ + fi + $(CMD_PREFIX) $(OC) create namespace $(UMAMI_KUBE_NAMESPACE) --dry-run=client -o yaml | $(OC) apply -f - + $(CMD_PREFIX) source .env && \ + deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh "OPENSHIFT" $(UMAMI_KUBE_NAMESPACE) + $(CMD_PREFIX) cat deploy/k8s/overlays/openshift/umami/umami-secret.yaml | kubeseal \ + --controller-name=${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace=${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + --format yaml > ./deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml + $(CMD_PREFIX) $(OC) apply -f deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml + $(CMD_PREFIX) $(OC) apply -k deploy/k8s/overlays/openshift/umami + $(CMD_PREFIX) echo "Waiting for Umami Deployment (pods: postgresql and umami) ..." + $(CMD_PREFIX) $(OC) wait --for=condition=Ready pods -n $(UMAMI_KUBE_NAMESPACE) --all -l app.kubernetes.io/part-of=umami --timeout=15m + $(CMD_PREFIX) umami_route=$$($(OC) get route umami -n $(UMAMI_KUBE_NAMESPACE) | tail -n 1 | awk '{print $$2}') ; \ + echo "Umami route deployed to: $$umami_route" + +.PHONY: undeploy-umami-prod-openshift +undeploy-umami-prod-openshift: + -$(CMD_PREFIX) $(OC) scale --replicas=0 deployment/umami -n $(UMAMI_KUBE_NAMESPACE) + -$(CMD_PREFIX) $(OC) delete -f ./deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml + -$(CMD_PREFIX) $(OC) delete -k ./deploy/k8s/overlays/openshift/umami + .PHONY: check-dev-container-installed check-dev-container-installed: @if [ -z "${DEVCONTAINER_BINARY_EXISTS}" ]; then \ - echo "You do not have devcontainer installed, please isntall it!"; \ - exit 1; \ + echo "You do not have devcontainer installed, please isntall it!" ; \ + exit 1 ; \ fi; .PHONY: build-dev-container @@ -233,12 +331,12 @@ cycle-dev-container: CONTAINER_IDS=$(shell ${CONTAINER_ENGINE} ps -a | grep "quay.io/instructlab-ui/devcontainer" | awk '{print $$1}') && \ if [ -n "$$CONTAINER_IDS" ]; then \ for CONTAINER_ID in "$$CONTAINER_IDS"; do \ - echo "Stopping and removing container $$CONTAINER_ID of imageid $$image_id..."; \ - ${CONTAINER_ENGINE} rm "$$CONTAINER_ID" -f; \ - done; \ - fi; \ - echo "removing image with id $$image_id and all containers using that image ..."; \ - ${CONTAINER_ENGINE} rmi $$image_id -f; \ + echo "Stopping and removing container $$CONTAINER_ID of imageid $$image_id..." ; \ + ${CONTAINER_ENGINE} rm "$$CONTAINER_ID" -f ; \ + done ; \ + fi ; \ + echo "removing image with id $$image_id and all containers using that image ..." ; \ + ${CONTAINER_ENGINE} rmi $$image_id -f ; \ fi; $(MAKE) build-dev-container $(MAKE) start-dev-container diff --git a/argocd/overlays/applicaitons/app-of-apps.yaml b/argocd/overlays/applicaitons/app-of-apps.yaml new file mode 100644 index 00000000..06c0cf75 --- /dev/null +++ b/argocd/overlays/applicaitons/app-of-apps.yaml @@ -0,0 +1,19 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-of-apps-ilab +spec: + destination: + namespace: openshift-gitpos + name: in-cluster + project: default + source: + path: argocd/overlays/applicaitons + repoURL: https://github.com/instructlab/ui.git + targetRevision: HEAD + syncPolicy: + syncOptions: + - Validate=false + - ApplyOutOfSyncOnly=true + # automated: + # selfHeal: true diff --git a/argocd/overlays/applicaitons/kustomization.yaml b/argocd/overlays/applicaitons/kustomization.yaml new file mode 100644 index 00000000..69d1d6bb --- /dev/null +++ b/argocd/overlays/applicaitons/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: openshift-gitops +resources: + # - prod.yaml # currently not deployed via argo + - qa.yaml + - umami.yaml diff --git a/argocd/overlays/applicaitons/prod.yaml b/argocd/overlays/applicaitons/prod.yaml index 067e0753..f787d5f6 100644 --- a/argocd/overlays/applicaitons/prod.yaml +++ b/argocd/overlays/applicaitons/prod.yaml @@ -4,7 +4,7 @@ metadata: name: ilab-ui-stack-production spec: destination: - name: in-cluster + name: in-cluster # THIS NEEDS TO CHANGE once we get prod on ARGO namespace: instructlab project: default source: diff --git a/argocd/overlays/applicaitons/umami.yaml b/argocd/overlays/applicaitons/umami.yaml new file mode 100644 index 00000000..e0b4c2a5 --- /dev/null +++ b/argocd/overlays/applicaitons/umami.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: umami +spec: + project: default + source: + repoURL: https://github.com/instructlab/ui.git + path: deploy/k8s/overlays/openshift/umami + targetRevision: main + destination: + namespace: umami + name: in-cluster + syncPolicy: + automated: + selfHeal: true + diff --git a/deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh b/deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh new file mode 100755 index 00000000..b1b89625 --- /dev/null +++ b/deploy/k8s/base/umami/deploy-umami-openshift-env-secret-conversion.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*- + +# Helper script to filter out `.env`` values related to umami deployment, and generate the secret manifest from that + +# Requires: kubectl, yq + +if [ -f ".env" ]; then + source .env +fi + +if [ "$#" -ne 2 ]; then + echo "USAGE: $0 TARGET NAMESPACE + TARGET: The deployment target. Options: [\"OPENSHIFT\", \"KIND\"] + NAMESPACE: The namespace where you want to deploy the umami-secret." 1>&2 + exit 1 +fi + +TARGET="$1" +NAMESPACE="$2" + +if [ "${TARGET}" == "OPENSHIFT" ]; then + UMAMI_SECRET_FILE_PATH="deploy/k8s/overlays/openshift/umami/umami-secret.yaml" + UMAMI_DATABASE_NAME_KEY_NAME=POSTGRESQL_DATABASE + UMAMI_DATABASE_USER_KEY_NAME=POSTGRESQL_USER + UMAMI_DATABASE_PASSWORD_KEY_NAME=POSTGRESQL_PASSWORD +elif [ "${TARGET}" == "KIND" ]; then + UMAMI_SECRET_FILE_PATH="deploy/k8s/overlays/kind/umami/umami-secret.yaml" + UMAMI_DATABASE_NAME_KEY_NAME=POSTGRES_DB + UMAMI_DATABASE_USER_KEY_NAME=POSTGRES_USER + UMAMI_DATABASE_PASSWORD_KEY_NAME=POSTGRES_PASSWORD +else + echo "Error, \$TARGET ${TARGET} not recongnized. + TARGET options: [\"OPENSHIFT\", \"KIND\"]" + exit 1 +fi + +required_vars=("DATABASE_TYPE" "UMAMI_DATABASE_NAME" "UMAMI_DATABASE_USER" "UMAMI_DATABASE_PASSWORD" "UMAMI_APP_SECRET" "DATABASE_URL") + +missing_vars=() + +for var in "${required_vars[@]}"; do + if [[ -z "${!var}" ]]; then + missing_vars+=("$var") + fi +done + +if [[ ${#missing_vars[@]} -gt 0 ]]; then + echo "The following environment variables are missing:" + for var in "${missing_vars[@]}"; do + echo " - $var" + done + echo "Please add these variables to your .env file." + exit 1 +fi + +cluster_domain=$(kubectl cluster-info | grep 'Kubernetes control plane' | awk -F// '{print $2}' | awk -F: '{print $1}') + +# Note: `.env` values get rerouted to their correct image target +# Prod uses: `POSTGRESQL_DATABASE`,`POSTGRESQL_USER`, and `POSTGRESQL_PASSWORD` +# Stage uses: `POSTGRES_DB`, `POSTGRES_USER` and `POSTGRES_PASSWORD` +# This different is due to the differences in the `postgresql:15-alpine` image and the `registry.redhat.io/rhel9/postgresql-15:9.5-1733127512` image +# Both map `UMAMI_APP_SECRET` to `APP_SECRET` + +kubectl create secret generic umami-secret \ + --from-literal "DATABASE_TYPE=${DATABASE_TYPE}" \ + --from-literal "${UMAMI_DATABASE_NAME_KEY_NAME}=${UMAMI_DATABASE_NAME}" \ + --from-literal "${UMAMI_DATABASE_USER_KEY_NAME}=${UMAMI_DATABASE_USER}" \ + --from-literal "${UMAMI_DATABASE_PASSWORD_KEY_NAME}=${UMAMI_DATABASE_PASSWORD}" \ + --from-literal "APP_SECRET=${UMAMI_APP_SECRET}" \ + --from-literal "DATABASE_URL=${DATABASE_URL}" \ + --namespace "${NAMESPACE}" \ + --dry-run=client \ + -o yaml > ${UMAMI_SECRET_FILE_PATH} + +yq eval ".metadata.labels.cluster_domain = \"${cluster_domain}\"" -i ${UMAMI_SECRET_FILE_PATH} + +echo "Secret manifest has been created: ${UMAMI_SECRET_FILE_PATH}." diff --git a/deploy/k8s/base/umami/deployment.yaml b/deploy/k8s/base/umami/deployment.yaml new file mode 100644 index 00000000..5fc0e8aa --- /dev/null +++ b/deploy/k8s/base/umami/deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: umami +spec: + replicas: 1 + strategy: + type: RollingUpdate + template: + spec: + containers: + - name: postgresql + image: postgres:15-alpine + env: + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: umami-secret + key: POSTGRES_DB + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: umami-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: umami-secret + key: POSTGRES_PASSWORD + ports: + - containerPort: 5432 + name: postgres + livenessProbe: + exec: + command: ["pg_isready"] + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: db-data + mountPath: /var/lib/postgresql/data + - name: umami + image: ghcr.io/umami-software/umami:postgresql-latest + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: umami-secret + key: DATABASE_URL + - name: DATABASE_TYPE + value: postgresql + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: umami-secret + key: APP_SECRET + - name: PORT + value: "3001" + ports: + - containerPort: 3001 + restartPolicy: Always + volumes: + - name: db-data + persistentVolumeClaim: + claimName: umami-postgresql-db-data diff --git a/deploy/k8s/base/umami/kustomization.yaml b/deploy/k8s/base/umami/kustomization.yaml new file mode 100644 index 00000000..afa29a2e --- /dev/null +++ b/deploy/k8s/base/umami/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: umami +resources: + - deployment.yaml + - namespace.yaml + - postgresql-pvc.yaml + - postgresql-service.yaml + - umami-service.yaml +labels: + - includeSelectors: true + pairs: + app: umami + app.kubernetes.io/component: umami + app.kubernetes.io/instance: umami + app.kubernetes.io/name: umami + app.kubernetes.io/part-of: umami diff --git a/deploy/k8s/base/umami/namespace.yaml b/deploy/k8s/base/umami/namespace.yaml new file mode 100644 index 00000000..196046b9 --- /dev/null +++ b/deploy/k8s/base/umami/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: umami diff --git a/deploy/k8s/base/umami/postgresql-pvc.yaml b/deploy/k8s/base/umami/postgresql-pvc.yaml new file mode 100644 index 00000000..3eaa5a4a --- /dev/null +++ b/deploy/k8s/base/umami/postgresql-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: umami-postgresql-db-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + volumeMode: Filesystem diff --git a/deploy/k8s/base/umami/postgresql-service.yaml b/deploy/k8s/base/umami/postgresql-service.yaml new file mode 100644 index 00000000..e4e88fb0 --- /dev/null +++ b/deploy/k8s/base/umami/postgresql-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: umami-db + labels: + component: db +spec: + ports: + - port: 5432 + name: postgres + selector: + app: umami diff --git a/deploy/k8s/base/umami/umami-service.yaml b/deploy/k8s/base/umami/umami-service.yaml new file mode 100644 index 00000000..47078319 --- /dev/null +++ b/deploy/k8s/base/umami/umami-service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: umami + labels: + component: web +spec: + ports: + - name: web + port: 3001 + selector: + app: umami + type: ClusterIP diff --git a/deploy/k8s/overlays/kind/umami/kustomization.yaml b/deploy/k8s/overlays/kind/umami/kustomization.yaml new file mode 100644 index 00000000..4f6bce68 --- /dev/null +++ b/deploy/k8s/overlays/kind/umami/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: umami +resources: + - ../../../base/umami + - umami-ingress.yaml diff --git a/deploy/k8s/overlays/kind/umami/umami-ingress.yaml b/deploy/k8s/overlays/kind/umami/umami-ingress.yaml new file mode 100644 index 00000000..c6b2da28 --- /dev/null +++ b/deploy/k8s/overlays/kind/umami/umami-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: umami-ingress + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: umami.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: umami + port: + number: 3001 diff --git a/deploy/k8s/overlays/openshift/umami/kustomization.yaml b/deploy/k8s/overlays/openshift/umami/kustomization.yaml new file mode 100644 index 00000000..20b76a85 --- /dev/null +++ b/deploy/k8s/overlays/openshift/umami/kustomization.yaml @@ -0,0 +1,33 @@ +# Umami will be deployed on the QA cluster but host metrics for both prod and QA +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: umami +resources: + - ../../../base/umami + - umami-route.yaml +patches: + - target: + kind: Deployment + name: umami + patch: |- + - op: replace + path: /spec/template/spec/containers/0/image + value: registry.redhat.io/rhel9/postgresql-15:9.5-1733127512 + - op: replace + path: /spec/template/spec/containers/0/env/0/name + value: POSTGRESQL_DATABASE + - op: replace + path: /spec/template/spec/containers/0/env/0/valueFrom/secretKeyRef/key + value: POSTGRESQL_DATABASE + - op: replace + path: /spec/template/spec/containers/0/env/1/name + value: POSTGRESQL_USER + - op: replace + path: /spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key + value: POSTGRESQL_USER + - op: replace + path: /spec/template/spec/containers/0/env/2/name + value: POSTGRESQL_PASSWORD + - op: replace + path: /spec/template/spec/containers/0/env/2/valueFrom/secretKeyRef/key + value: POSTGRESQL_PASSWORD diff --git a/deploy/k8s/overlays/openshift/umami/umami-route.yaml b/deploy/k8s/overlays/openshift/umami/umami-route.yaml new file mode 100644 index 00000000..bc4e5c49 --- /dev/null +++ b/deploy/k8s/overlays/openshift/umami/umami-route.yaml @@ -0,0 +1,17 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: umami + labels: + name: umami +spec: + port: + targetPort: 3001 + tls: + termination: edge + to: + kind: Service + name: umami + weight: 100 + wildcardPolicy: None + diff --git a/deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml b/deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml new file mode 100644 index 00000000..7b14c242 --- /dev/null +++ b/deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml @@ -0,0 +1,22 @@ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: umami-secret + namespace: umami +spec: + encryptedData: + APP_SECRET: AgAqI0ncJPD7kW3hjNvw+y7O1nwqugOZ/0lAfpQPw/bTQEdmc6TLDO3jsaPqMqibE9rgCc9bqwUst7ftadbEEgaiy5n6dm7K5FlsdqskG5sRLYAtNjEOEGgdV09hAc5oQPFTI65BVlrebk+brfRrnlN1ugrYBtDbxs9SW36Vz+wLNYM3bG9LlViOag30xVCJSA8a/GLNh2eUiHppjvV4SIvOrrgN8wp9RaiVUoDoRpwOiGeiBNhLkpC6R9CcS0aUSbIp2inSMLnjJ7gOSKblxDcV1GN+yTNOedmAQDGhx1KiKUPhokE6FTG/v++ALcBz4LbJvCMyXiDWHuP1YIPhOyXFmHaS+EclOJ6a8+NJbs/tRnhngHLT5vcY0I8QLHL8n5eMcEeNt9DbS3B7j+ytbvZZydeIo4u2wCotu+wz33UlnSaOvmEvgZKIAVF7Yt9qg7NvHlWfHb2BP7Cu8lDAGvItpD5V4GnfvmzaNe3DvMYz0/IzEBA6YJpv9OK5P8Vlc/waj0+6e+MctzFPo3adICV5uqOITQ95fzyvuaaEhZX3ktj8M3diqUTWujvl2fYAMT8SU933UUI1HbNpugMa4MRgim+7C7zJ0XYONS6bFNYjls56L7DW3WcZda4lqMA4bfli1jH+rAdpZcXZ6iYpmA0Uga4LUT/Kr5iiVIfu5llgriHP9DABbV5zLcpKBtX5kqrQpNi+zmqNQsV3 + DATABASE_TYPE: AgANKG5Gvz0dgHBf15dBEdGNomCIPgPRhuNvDzmj6j/6LWeBIAAFNLS+zwsSr0FvVIkc5a3ruTf2fBj0wrL0xrshKaTq03UXdJJtAESYRV/whsU9ldfXEXtcsux8jMbFIaI17PFDP9KRh6MwhtI4TJfruyiQbPlVr4a2qxoDxQvj6iLV80INaQs1lO+DTJc0nKStJMzhdpqpAmEZW6JEl5N9r/y3IDhHK3Jz13LNHyeIfIfFVBJ51cAkCJZV2alFA0jRTLhBn/YSBiZy9a0fIQzooBxO+1kTCl2OzZKH3wYHb2ZS5RmIa5lwHnTiGFzgc5L8T1N3zt4cTYhVIYEF/8mL/GXZxEKYOJuBLSnPzwUGE/dRlgnFbqtW6aoDDDj3Ma1iqh2GhRPjSviYXmL5Iwi4bj279yU/YCI3y0LZBYNKrYjAoY9rMdNoFOxPpIqna3g2gAmzsTba9YbzJUAnQYkSjgT6wT5/jUIO+ierWdHsWi/sw+0v4DWed0ERC8TKf9F70o+5ET62NMVjeqpSG7S9k7mbCf9PeObc15eYLwbC29pnBBzJv077TKE7tVu7PpwbSK37mGg+YB5AIMPmVgIVByqko7sW00KnWLcwt50/mYsoaVtzdNNKTGj+j8tXXijQx3csT7hzM1aWOltvgd38vZiGgA+Hm1S9HVLjpdig6F0mjwe9Ft//Jlvsp6ebxhuPJZGAp6uSbMNk + DATABASE_URL: AgAmHPfviY9wE4aFtZD1RWuz2hkx27B2oNyQP9SWPuhOA18nNL479a3ECMfOp9jSNVjn7YABYq044bolLDgnZx6R8rsPLp6Uf1Mr0ifCFxZcz0Whn8rDtbzV1sq2Ef9xiW/4OpxQmxEYQpg5aDgnE3GmoDb+22ry0l1B48tD2aOIn4rhI6MOxG3LtLeuP78dxozGVFBwGc3zqgXYaxVSwg26ZVQoMbNgAkEdxHgthmVlBapktsPtEgrskVUTsFWX3TsoPwts3+YmiyaUYxf/Rjy9CMjJbvxf98z3nhW33R1p+YgjvexeVSSDcJcZoFmpViLZbO+I1coe/xmn3iaKqKcVp3cpz/4SX8NIipFvIvvlcwQR9wPTV04PO9OukkyYCY740FYk+4KBfAzw/9NmrIkrAB/lNkanGXoHA+QqfGRXyqUm5LJ4M+nNbXP1ewY/8tQvC1dohig9WiSPv1njnQT3LbAbr3WK6vMKGjhscXv6c4CfM0iPf+5DR+yIwL9///9I21SMTLGLVWS192NPaLda5zqDuvvdRQzsW6B99BvXRd1x6pJX6TOVG/Ar51SgR9vYdMuca0xw8FlMvFqx8jvfPrvwgB65FLFCic2PXplhs7TCARCB+YYZfZQxhqfXbImCg2R+cgq2qCeqGLSOxQ4dddtXP3sJV4NH2j4k5Fbx0JR36FAb3eN2qhT+jsDT98TkyZNsUWMbG9a5nRE3QMfC1TM4hgqWvekUm/gqwkIY7cXJOh9o79i5XI4fag== + POSTGRESQL_DATABASE: AgBKzY68zFlxv0tBV/yBvbGlkYz8zqCHnDojFoCwfQ5QChJyTk6sIHFbarfRIPA2Mset8SXMXtchAfPauTmLfqiM+x+Sb48L+A8eTiIg+YUeeRWF+GsPBgxAiA3HS1nBDrfjQgl8YYjBhaEfSDKWuHIeP34vFcuLi3M6yzjIbOXQOeLfX+L9QGgB6vhgeAU5zAnU115Dw9d+MAfqaK5heeFgn2WSoZs6cTuFjFRg+paZpiE78dLd5fIcVA5dptDvFZv/P4pOhX4klshQB9cn9CBharbNJ5QPWkUyEewD5yXGhVPFPIjCD95RYQFVsRhB9q5QZ0nKgdHxPAjJDY92m2Ks3XF14mLMIl+T40nwaNZpNrlxT99feRSlkzvVUHGggbVdxTvieIfDmcED8lcek0RKnkYd/DAYDPhEA+Zbgl+YHVyiO9KKcrq1Vs2zmkA1nwjRTDMcgTcAmwHZhQqwipU8SxzU0PFHxBlVuCe5+lYXzzXbolfEH14fQMbge5SVufqUBH35k8vu/u4aAvEhRmcy8TzPdYFR5F0h3YccRzI0Crj7fzUIV2jg4ThEo64OQzVR0qNKQeuR+Naplo4J3Whr9WYdac3X8A5gasTz/kwMyFfntReRgrq5BWetM8lcpkoCmqI/jEFL9gsKT1MbeDcRDOiU4tvCnhMxu9tUFvptHQ9MFEO+6vqF0ULqrW04wT2VbnHF8w== + POSTGRESQL_PASSWORD: AgBHJCdFNapk+siNYrXrg0DhUEW6vflZMfD64g2rjZy0Mv5GSWQzGM9L6bs596CEb5dzP7D63B4sfDhAA5ikrSTj0Hcr0QtByRCTouRl6X2XqaIIO2DednTe0CFmQISrdw38D8zYBTe2aHyEHgh/d/BeJlVsPvzqyYRXg1LKzozC2rz/B8GEnqRy1iZnprt7Y5BDtUiWnqlRUhrB2i6oOVXqptF1uIgBLqGqllofqG2U2BFWmYYzj/0tvttMj65THufCkJ88SLaQk5lo1h9i6DEPdSly8n942pDtn9ivpQ2pCoxx6U3j0XPWeJh6kmR0c5aTrZGH4BBQb8FGKkLCorcIwJXGGakcXcqAKLBHxmRd5XWT7LuVUWJkwp5FmqBDoc1RQdzLIPAdg3c8V8Cg2ouLrNue+jtVkKWgiKJBs5R05bjJ/umjKS6MUpqmXYbBjfyLUBrmQgf+v9IimnJC3nAEX+Z3TgEmSDv4du4r4f1i8VFknOWHe51+Ocdo2n4+TVuS2YrAN7P/Q8bGmgUEgLs/Qpg44ikEqMwmd9g7mlUnDZrBXD3OMN6XfwtPOaSbC8kzYmEkCMY5r0D7FpW49kNSZzdksnzv726MqJlekZ4XTjGOmVRi0Nu59Ck+Y698cELhkPBNnFuSiT8sWQLQ0sKUo21yrzqnT30pLinR/Rs1VODkpaPNAtWWdMQQGAVUJmveUbF1qw== + POSTGRESQL_USER: AgBP0Ecq1alJMZ8jkT8C1RaEX5euu4wnXoRr433sCXGICLjvhHNHp7qgXxJ5R9R3RWFxl/cyWyySm5hEtVgBQMJDKBfMWwLrK/qYywVlC2UzHXHzpHE9OmaKb014GlnQPAHw4s40bOmBUvXM0eC7m630TQgYTAqWoHHgReSVpvQvyBs6aP/E1BW+vb0bRxYmNqDWJrG5wIiPhHjD0ipLE1cOvyaCFmYaGcdTiMaAu4bBdMkGn7D6lvqMvKfzEON+1tw+FPWubfVgnx90oGOehsAtzU1UcCK2LXfGauoTHhqZqPwA32h2tnmf8CQ8ZNN4cXAeWKyq8xFbHhDktA7ev7d7ObMZLH5OLIIZd+gtV0q80TMG5vhRemDFs8AL19FkGsGZEdQISBfmGiCU8AQ5xKuYI8UnsHaDN8uhUhUdsJ7xKi8FOzWW1rQu1eRYCjTnccX85AoPBg9fZBPoCE1FRDZK72jZMXD+DHHVxWjSsuSt870N+e6aqB0Sia3Bq13n/Z71Datb1hgo1epbKCrbS7azUh3tlaZKUtOhAyTLcm6jwW5V6tqPaqhWz+XVLFglNwIBXcmjC3BjNhBAgi2m+OQsKnZb280GBn9+v0EkYKPY6Jmo5D0vdjPjIkXIZGYbTQZyEv0iZqT6R/dESLrAqwMq9ycR5QNsHdw/im+6Ci02dbi+fbM7zBNhRcI/OYBIWDt3uqAKqw== + template: + metadata: + creationTimestamp: null + labels: + cluster_domain: c130-e.us-south.containers.cloud.ibm.com + name: umami-secret + namespace: umami + diff --git a/docs/umami_metrics.md b/docs/umami_metrics.md new file mode 100644 index 00000000..45cb73eb --- /dev/null +++ b/docs/umami_metrics.md @@ -0,0 +1,100 @@ +# Umami metrics + +Umami is an open-source, privacy-focused web analytics tool that serves as an alternative to Google Analytics. It provides essential insights into website traffic, +user behavior, and performance, all while prioritizing data privacy. We chose to use Umami as our perfered method of metrics collection and visualisation due to +ability to support for self-hosted deployments, open-source nature, and ease of use. Check out [their docs](https://umami.is/docs) for more information. + +## Deployment + +Umami is meant to work with either a `mysql` or `postgresql` DB backend. For now we only provide manifests and options for PostgreSQL, +but if the need arises we will extend this to work with either. + +### Required ENV values + +To deploy the stack, there are some ENV values you must set. Technically speaking the `UMAMI_APP_SECRET` is not required, because the stack +[will use the `DATABASE_URL` instead](https://github.com/umami-software/umami/blob/master/src/lib/crypto.ts#L6) if an `APP_SECRET` is not provided +but best security practices are to set it. + +Also, we have chosen to use `UMAMI_APP_SECRET` in the `.env` file but in the deployment process this gets mapped to `APP_SECRET`. We chose this +pattern because it brings clarity to what the variable does in the context of the `.env` file. + +| Variable | Description | Example Value | +|-------------------------|---------------------------------------------------------------------------|----------------------------------------------------| +| UMAMI_APP_SECRET | Used as Hash Salt for the Database | YbSbtb | +| DATABASE_TYPE | Type of Database to use with Umami. Only `postgresql` currently supported | postgresql | +| UMAMI_DATABASE_NAME | Name of the database backend for Umami | db-name | +| UMAMI_DATABASE_USER | Name of the user of the database for Umami | db-user | +| UMAMI_DATABASE_PASSWORD | Password for the user of the database for Umami | db-pass | +| DATABASE_URL | The URL the Umami pod will use to access the DB | postgresql://db-user:db-pass@umami-db:5432/db-name | + +> [!IMPORTANT] +> The `DATABASE_URL` is derrived from the other variables plus the [name of the service](../deploy/k8s/base/umami/postgresql-service.yaml#L4) used in deployment. +> The env variables `UMAMI_DATABASE_NAME`, `UMAMI_DATABASE_USER` and `UMAMI_DATABASE_PASSWORD` get mapped to the environment variables for the container image +> used based the environment. +> For `kind` these are `POSTGRES_DB`, `POSTGRES_USER` and `POSTGRES_PASSWORD` respectively. +> For `openshift` environment these are `POSTGRESQL_DATABASE`, `POSTGRESQL_NAME` and `POSTGRESQL_PASSWORD` respectively. + +Place those required variables in the `.env` file in the root of the repo. + +### Deployment Manifest Notes + +In the [base deployment mainfest](../deploy/k8s/base/umami/deployment.yaml) the command is provided to the `umami` container to delay its start. This is +because the `umami` container crashloops while it waits for the `postgresql` container to come online. Ideally it woudl use a `livelinessProbe` or +`readinessProbe` but the `umami` container lacks proper networking tools, and there are no endpoints for `/health` or `/metrics` on that contianer to do +a vanila `curl`. In my testing with this `sleep` there are no crashes, but if your cluster is slower this may restart once or twice. In future we should +create our own image from the `ghcr.io/umami-software/umami:postgresql-latest` and add networking tools to detect if the psql container is up to avoid +annoying restart crashloops. + +### Make Targets + +Make targets are our prefered method of deployment. + +This section will cover how the make targets work and how they differ per environment. The umami deployment `make` targets for all 3 environments use a +[conversion script](./deploy/k8s/overlays/kind/umami/umami-secret.yaml) to parse values out of the `.env` file, into their own secret created in the +respective overlay directory (`deploy/k8s/overlays`). These secrets will be ignored in `git` and are not included in their respective `kustomization.yaml` +overlay files - they must be applied indivdually. This is done because for the Ilab-teams hosted deployments ([ui.instructlab.ai](https://ui.instructlab.ai/) +and [qa.ui.instructlab.ai](https://qa.ui.instructlab.ai/)) we want to track those manifests in `git` via an encrypted sealed-secret, but also allow the +deployment to work out of the box for people trying to self-deploy the stack.This creates a straightforward experience for both developers and maintainers. + +#### Kind + +Pre-requisite: `make setup-kind` +Command: `make deploy-umami-kind` + +After your kind cluster has been started (`make setup-kind`), you can use `make deploy-umami-kind`, which will take care of everything. +The umami-secret will be created at path `deploy/k8s/overlays/kind/umami/umami-secret.yaml`, and deploy it, along with the `./deploy/k8s/overlays/kind/umami` +overlay manifests. Finally it will wait for the pods to rollout and then preform portforwarding on port `3001` for the Umami service. + +It should be noted that `kind` deployment uses the base manifest's `postgres:15-alpine` database image, with its respective `env` values: `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD`. + +#### QA + +Command: `make deploy-umami-qa-openshift` + +This will create the umami-secret at path `deploy/k8s/overlays/openshift/umami/umami-secret.yaml`. This is very similar to the `kind` umami deployment target +except that it will deploy a `route` instead of an ingress. + +It should be noted that `qa-openshift` deployment uses the base manifest's `registry.redhat.io/rhel9/postgresql-15:9.5-1733127512` database image, with its respective `env` values: `POSTGRESQL_DATABASE`, `POSTGRESQL_NAME`, and `POSTGRESQL_PASSWORD`. + +#### Prod + +Command: `make deploy-umami-prod-openshift` + +This will use the same secret path as QA `deploy/k8s/overlays/openshift/umami/umami-secret.yaml`. However, instead of applying the secret, it will pipe +that secret to the `kubeseal` binary, which will pass it to the sealed secrets operator deployed in the cluster. If you have a custom namespace or sealed +secrets controller name, make sure to update the `SEALED_SECRETS_CONTROLLER_NAMESPACE` and `SEALED_SECRETS_CONTROLLER_NAME` values at the top of the +[Makefile](../Makefile#L27-28). If successful, this will encrypt the secret to create the +[umami-secret.sealedsecret.yaml](../deploy/k8s/overlays/openshift/umami/umami-secret.sealedsecret.yaml) which can safely get tracked in `git`. Finally, +it will apply the sealed secret and the rest of the manifests. + +It should be noted that `prod-openshift` deployment uses the base manifest's `registry.redhat.io/rhel9/postgresql-15:9.5-1733127512` database image, with its respective `env` values: `POSTGRESQL_DATABASE`, `POSTGRESQL_NAME`, and `POSTGRESQL_PASSWORD`. + +## Administration + +When Umami gets deployed, it will have no configurations. The admin will have to login with the default Umami credentials, setup users and teams for access, +and change the default admin password. For information on how this works, refer to [that section of the Umami docs](https://umami.is/docs/login). Currently +there is no way to apply manifests for operations and configurations like this, so this is a manual process, and would need to be redone if the deployment +goes down. + +Once teams and users are properly setup, setup a `site` for each environment we want to deploy. Once created it will provision a script tag to inject +into the typescript code to start tracking metrics.