diff --git a/.gitignore b/.gitignore index 5c32dd18..d535bc69 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,10 @@ Thumbs.db *.pyo *.pyd __pycache__/ + +# wg-easy specific +*.kubeconfig +applications/wg-easy/release/ +applications/wg-easy/*/charts/ +applications/wg-easy/*/Chart.lock +.aider* diff --git a/applications/wg-easy/README.md b/applications/wg-easy/README.md new file mode 100644 index 00000000..b6ef8ddd --- /dev/null +++ b/applications/wg-easy/README.md @@ -0,0 +1,91 @@ +# WG-Easy Helm Chart Development Pattern + +This repository demonstrates a structured approach to developing and deploying Helm charts with Replicated integration. It focuses on a progressive development workflow that builds complexity incrementally, allowing developers to get fast feedback at each stage. + +## Core Principles + +The WG-Easy Helm Chart pattern is built on five fundamental principles: + +### 1. Progressive Complexity + +Start simple with individual chart validation and progressively move to more complex environments. This allows issues to be caught early when they are easier to fix. + +- Begin with local chart validation +- Move to single chart deployments +- Progress to multi-chart integration +- Finally test in production-like environments + +### 2. Fast Feedback Loops + +Get immediate feedback at each development stage by automating testing and validation. This shortens the overall development cycle. + +- Automated chart validation +- Quick cluster creation and deployment +- Standardized testing at each stage +- Fast iteration between changes + +### 3. Reproducible Steps + +Ensure consistent environments and processes across all stages of development, eliminating "works on my machine" issues. + +- Consistent chart configurations +- Automated environment setup +- Deterministic dependency management +- Standardized deployment procedures + +### 4. Modular Configuration + +Allow different components to own their configuration independently, which can be merged at release time. + +- Per-chart configuration files +- Automatic configuration merging +- Clear ownership boundaries +- Simplified collaborative development + +### 5. Automation First + +Use tools to automate repetitive tasks, reducing human error and increasing development velocity. + +- Task-based workflow automation +- Helmfile for orchestration +- Automated validation and testing +- Streamlined release process + +## Repository Structure + +``` +applications/wg-easy/ +├── charts/templates/ # Common templates shared across charts +├── cert-manager/ # Wrapped cert-manager chart +├── cert-manager-issuers/ # Chart for cert-manager issuers +├── replicated/ # Root Replicated configuration +├── replicated-sdk/ # Replicated SDK chart +├── taskfiles/ # Task utility functions +├── traefik/ # Wrapped Traefik chart +├── wg-easy/ # Main application chart +├── helmfile.yaml # Defines chart installation order +└── Taskfile.yaml # Main task definitions +``` + +## Architecture Overview + +![Architecture Diagram](docs/architecture.png) + +Key components: +- **Taskfile**: Orchestrates the workflow with automated tasks +- **Helmfile**: Manages chart dependencies and installation order +- **Wrapped Charts**: Encapsulate upstream charts for consistency +- **Shared Templates**: Provide reusable components across charts +- **Replicated Integration**: Enables enterprise distribution + +## Learn More + +- [Chart Structure Guide](docs/chart-structure.md) +- [Development Workflow](docs/development-workflow.md) +- [Task Reference](docs/task-reference.md) +- [Replicated Integration](docs/replicated-integration.md) +- [Example Patterns](docs/examples.md) + +--- + +This pattern is designed to be adaptable to different applications and requirements. Feel free to modify it to suit your specific needs. \ No newline at end of file diff --git a/applications/wg-easy/Taskfile.yaml b/applications/wg-easy/Taskfile.yaml new file mode 100644 index 00000000..ae079c6e --- /dev/null +++ b/applications/wg-easy/Taskfile.yaml @@ -0,0 +1,359 @@ +version: "3" + +includes: + utils: ./taskfiles/utils.yml + +vars: + # Application configuration + APP_NAME: '{{.REPLICATED_APP | default "wg-easy"}}' + + # Cluster configuration + CLUSTER_NAME: '{{.CLUSTER_NAME | default "test-cluster"}}' + K8S_VERSION: '{{.K8S_VERSION | default "1.32.2"}}' + DISK_SIZE: '{{.DISK_SIZE | default "100"}}' + INSTANCE_TYPE: '{{.INSTANCE_TYPE | default "r1.small"}}' + DISTRIBUTION: '{{.DISTRIBUTION | default "k3s"}}' + KUBECONFIG_FILE: './{{.CLUSTER_NAME}}.kubeconfig' + + # Ports configuration + EXPOSE_PORTS: + - port: 30443 + protocol: https + - port: 30080 + protocol: http + + # GCP default configuration + GCP_PROJECT: '{{.GCP_PROJECT | default "replicated-qa"}}' + GCP_ZONE: '{{.GCP_ZONE | default "us-central1-a"}}' + VM_NAME: '{{.VM_NAME | default (printf "%s-dev" (or (env "GUSER") "user"))}}' + +tasks: + default: + desc: Show available tasks + cmds: + - task -s --list + + cluster-create: + desc: Create a test cluster using Replicated Compatibility Matrix (use EMBEDDED=true for embedded clusters) + run: once + silent: false + vars: + EMBEDDED: '{{.EMBEDDED | default "false"}}' + LICENSE_ID: '{{if eq .EMBEDDED "true"}}{{.LICENSE_ID | default "2cmqT1dBVHZ3aSH21kPxWtgoYGr"}}{{end}}' + TIMEOUT: '{{if eq .EMBEDDED "true"}}420{{else}}300{{end}}' + status: + - replicated cluster ls --output json | jq -e '.[] | select(.name == "{{.CLUSTER_NAME}}")' > /dev/null + cmds: + - | + if [ "{{.EMBEDDED}}" = "true" ]; then + echo "Creating embedded cluster {{.CLUSTER_NAME}} with license ID {{.LICENSE_ID}}..." + replicated cluster create --distribution embedded-cluster --name {{.CLUSTER_NAME}} --license-id {{.LICENSE_ID}} + else + echo "Creating cluster {{.CLUSTER_NAME}} with distribution {{.DISTRIBUTION}}..." + replicated cluster create --name {{.CLUSTER_NAME}} --distribution {{.DISTRIBUTION}} --version {{.K8S_VERSION}} --disk {{.DISK_SIZE}} --instance-type {{.INSTANCE_TYPE}} + fi + - task: utils:wait-for-cluster + vars: + TIMEOUT: "{{.TIMEOUT}}" + + list-cluster: + desc: List the cluster + silent: false + cmds: + - | + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .id') + EXPIRES=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .expires_at') + echo "{{.CLUSTER_NAME}} Cluster ID: ($CLUSTER_ID) Expires: ($EXPIRES)" + + test: + desc: Run a basic test suite + silent: false + cmds: + - echo "Running basic tests..." + - echo "This is a placeholder for actual tests" + - sleep 5 + - echo "Tests completed!" + + + verify-kubeconfig: + desc: Verify kubeconfig + silent: false + run: once + cmds: + - | + if [ -f {{.KUBECONFIG_FILE}} ]; then + echo "Getting Cluster ID From Replicated Cluster list" + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .id') + echo "Getting Cluster ID From Kubeconfig" + CLUSTER_ID_KUBECONFIG=$(grep "current-context:" {{.KUBECONFIG_FILE}} | cut -d'-' -f3) + if [ "$CLUSTER_ID" != "$CLUSTER_ID_KUBECONFIG" ]; then + echo "{{.CLUSTER_NAME}} Cluster ID between Replicated ($CLUSTER_ID) and Kubeconfig ($CLUSTER_ID_KUBECONFIG) mismatch" + echo "Removing old kubeconfig file" + rm -f {{.KUBECONFIG_FILE}} + fi + fi + + setup-kubeconfig: + desc: Get kubeconfig and prepare cluster for application deployment + silent: false + run: once + cmds: + - task: utils:get-kubeconfig + - task: utils:remove-k3s-traefik + status: + - | + # Check if kubeconfig exists + test -f {{.KUBECONFIG_FILE}} && \ + # For k3s, also check if traefik is removed + if [ "{{.DISTRIBUTION}}" = "k3s" ]; then + KUBECONFIG={{.KUBECONFIG_FILE}} helm list -n kube-system -o json | \ + jq -e 'map(select(.name == "traefik" or .name == "traefik-crd")) | length == 0' >/dev/null + else + true + fi + deps: + - create-cluster + - verify-kubeconfig + + dependencies-update: + desc: Update Helm dependencies for all charts + silent: false + cmds: + - echo "Updating Helm dependencies for all charts..." + - | + # Find all charts and update their dependencies + for chart_dir in $(find . -maxdepth 2 -name "Chart.yaml" | xargs dirname); do + echo "Updating dependency $chart_dir" + helm dependency update "$chart_dir" + done + - echo "All dependencies updated!" + + ports-expose: + desc: Expose configured ports and capture exposed URLs + silent: false + run: once + status: + - | + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .id') + if [ -z "$CLUSTER_ID" ]; then + exit 1 + fi + + # Check if all ports are already exposed + expected_count={{len .EXPOSE_PORTS}} + port_checks="" + {{range $i, $port := .EXPOSE_PORTS}} + port_checks="${port_checks}(.upstream_port == {{$port.port}} and .exposed_ports[0].protocol == \"{{$port.protocol}}\") or " + {{end}} + # Remove trailing "or " + port_checks="${port_checks% or }" + + PORT_COUNT=$(replicated cluster port ls $CLUSTER_ID --output json | jq -r ".[] | select($port_checks) | .upstream_port" | wc -l | tr -d ' ') + [ "$PORT_COUNT" -eq "$expected_count" ] + cmds: + - task: utils:port-operations + vars: + OPERATION: "expose" + deps: + - cluster-create + + helm-deploy: + desc: Deploy all charts using helmfile + silent: false + cmds: + - echo "Installing all charts via helmfile" + - | + # Get cluster ID + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .id') + if [ -z "$CLUSTER_ID" ]; then + echo "Error: Could not find cluster with name {{.CLUSTER_NAME}}" + exit 1 + fi + + # Get exposed URLs + ENV_VARS=$(task utils:port-operations OPERATION=getenv CLUSTER_NAME={{.CLUSTER_NAME}}) + + # Deploy with helmfile + echo "Using $ENV_VARS" + eval "KUBECONFIG={{.KUBECONFIG_FILE}} $ENV_VARS helmfile sync --wait" + - echo "All charts deployed!" + deps: + - setup-kubeconfig + - ports-expose + + + cluster-delete: + desc: Delete all test clusters with matching name and clean up kubeconfig + silent: false + cmds: + - echo "Deleting clusters named {{.CLUSTER_NAME}}..." + - | + CLUSTER_IDS=$(replicated cluster ls | grep "{{.CLUSTER_NAME}}" | awk '{print $1}') + if [ -z "$CLUSTER_IDS" ]; then + echo "No clusters found with name {{.CLUSTER_NAME}}" + exit 0 + fi + + for id in $CLUSTER_IDS; do + echo "Deleting cluster ID: $id" + replicated cluster rm "$id" + done + - | + # Clean up kubeconfig file + if [ -f "{{.KUBECONFIG_FILE}}" ]; then + echo "Removing kubeconfig file {{.KUBECONFIG_FILE}}" + rm "{{.KUBECONFIG_FILE}}" + fi + - echo "All matching clusters deleted and kubeconfig cleaned up!" + + release-prepare: + desc: Prepare release files by copying replicated YAML files and packaging Helm charts + silent: false + cmds: + - echo "Preparing release files..." + - rm -rf ./release + - mkdir -p ./release + + # Copy all non-config.yaml files + - echo "Copying non-config YAML files to release folder..." + - find . -path '*/replicated/*.yaml' -not -name 'config.yaml' -exec cp {} ./release/ \; + - find ./replicated -name '*.yaml' -not -name 'config.yaml' -exec cp {} ./release/ \; 2>/dev/null || true + + # extract namespaces from helmChart files + - yq ea '[.spec.namespace] | unique' */replicated/helmChart-*.yaml | yq '.spec.additionalNamespaces *= load("/dev/stdin") | .spec.additionalNamespaces += "*" ' replicated/application.yaml > release/application.yaml.new + - mv release/application.yaml.new release/application.yaml + + # set helmChart versions from associated helm Chart.yaml + - echo "Setting helmChart versions..." + - | + while read directory; do + + echo $directory + parent=$(basename $(dirname $directory)) + + helmChartName="helmChart-$parent.yaml" + export version=$(yq -r '.version' $parent/Chart.yaml ) + + yq '.spec.chart.chartVersion = strenv(version) | .spec.chart.chartVersion style="single"' $directory/$helmChartName | tee release/$helmChartName + + done < <(find . -maxdepth 2 -mindepth 2 -type d -name replicated) + + # Merge config.yaml files + - echo "Merging config.yaml files..." + - | + # Start with an empty config file + echo "{}" > ./release/config.yaml + + # Merge all app config.yaml files first (excluding root replicated) + for config_file in $(find . -path '*/replicated/config.yaml' | grep -v "^./replicated/"); do + echo "Merging $config_file..." + yq eval-all '. as $item ireduce ({}; . * $item)' ./release/config.yaml "$config_file" > ./release/config.yaml.new + mv ./release/config.yaml.new ./release/config.yaml + done + + # Merge root config.yaml last + if [ -f "./replicated/config.yaml" ]; then + echo "Merging root config.yaml last..." + yq eval-all '. as $item ireduce ({}; . * $item)' ./release/config.yaml "./replicated/config.yaml" > ./release/config.yaml.new + mv ./release/config.yaml.new ./release/config.yaml + fi + + # Package Helm charts + - echo "Packaging Helm charts..." + - | + # Find top-level directories containing Chart.yaml files + for chart_dir in $(find . -maxdepth 2 -name "Chart.yaml" | xargs dirname); do + echo "Packaging chart: $chart_dir" + # Navigate to chart directory, package it, and move the resulting .tgz to release folder + (cd "$chart_dir" && helm package . && mv *.tgz ../release/) + done + + - echo "Release files prepared in ./release/ directory" + deps: + - update-version + + + release-create: + desc: Create and promote a release using the Replicated CLI + silent: false + vars: + CHANNEL: '{{.CHANNEL | default "Unstable"}}' + RELEASE_NOTES: '{{.RELEASE_NOTES | default "Release created via task release-create"}}' + cmds: + - echo "Creating and promoting release for $APP_NAME to channel $CHANNEL..." + - | + # Create and promote the release in one step + echo "Creating release from files in ./release directory..." + replicated release create --app $APP_NAME --yaml-dir ./release --release-notes "$RELEASE_NOTES" --promote $CHANNEL --version $VERSION + echo "Release version $VERSION created and promoted to channel $CHANNEL" + deps: + - release-prepare + + gcp-vm-create: + desc: Create a simple GCP VM instance + silent: false + vars: + GCP_MACHINE_TYPE: '{{.GCP_MACHINE_TYPE | default "e2-standard-2"}}' + GCP_DISK_SIZE: '{{.GCP_DISK_SIZE | default "100"}}' + GCP_DISK_TYPE: '{{.GCP_DISK_TYPE | default "pd-standard"}}' + GCP_IMAGE_FAMILY: '{{.GCP_IMAGE_FAMILY | default "ubuntu-2204-lts"}}' + GCP_IMAGE_PROJECT: '{{.GCP_IMAGE_PROJECT | default "ubuntu-os-cloud"}}' + status: + - gcloud compute instances describe {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} &>/dev/null + cmds: + - task: utils:gcp-operations + vars: + OPERATION: "create" + GCP_MACHINE_TYPE: '{{.GCP_MACHINE_TYPE}}' + GCP_DISK_SIZE: '{{.GCP_DISK_SIZE}}' + GCP_DISK_TYPE: '{{.GCP_DISK_TYPE}}' + GCP_IMAGE_FAMILY: '{{.GCP_IMAGE_FAMILY}}' + GCP_IMAGE_PROJECT: '{{.GCP_IMAGE_PROJECT}}' + + gcp-vm-delete: + desc: Delete the GCP VM instance for K8s and VPN + silent: false + status: + - "! gcloud compute instances describe {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} &>/dev/null" + cmds: + - task: utils:gcp-operations + vars: + OPERATION: "delete" + GCP_PROJECT: '{{.GCP_PROJECT}}' + GCP_ZONE: '{{.GCP_ZONE}}' + VM_NAME: '{{.VM_NAME}}' + + embedded-cluster-setup: + desc: Setup Replicated embedded cluster on the GCP VM + silent: false + vars: + CHANNEL: '{{.CHANNEL | default "Unstable"}}' + AUTH_TOKEN: '{{.AUTH_TOKEN | default "2usDXzovcJNcpn54yS5tFQVNvCq"}}' + deps: + - gcp-vm-create + status: + - | + # Check if the application tarball has already been downloaded and extracted + gcloud compute ssh {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} --command="test -d ./{{.APP_NAME}}" &>/dev/null + cmds: + - task: utils:gcp-operations + vars: + OPERATION: "setup-embedded" + APP_NAME: '{{.APP_NAME}}' + CHANNEL: '{{.CHANNEL}}' + AUTH_TOKEN: '{{.AUTH_TOKEN}}' + GCP_PROJECT: '{{.GCP_PROJECT}}' + GCP_ZONE: '{{.GCP_ZONE}}' + VM_NAME: '{{.VM_NAME}}' + + full-test-cycle: + desc: Create cluster, get kubeconfig, expose ports, update dependencies, deploy charts, test, and delete + silent: false + cmds: + - task: cluster-create + - task: setup-kubeconfig + - task: ports-expose + - task: dependencies-update + - task: helm-deploy + - task: test + - task: cluster-delete diff --git a/applications/wg-easy/cert-manager-issuers/Chart.yaml b/applications/wg-easy/cert-manager-issuers/Chart.yaml new file mode 100644 index 00000000..b94c9b7c --- /dev/null +++ b/applications/wg-easy/cert-manager-issuers/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: cert-manager-issuers +description: A Helm chart for cert-manager issuers +type: application +version: 1.0.0 +appVersion: "1.0.0" diff --git a/applications/wg-easy/cert-manager-issuers/replicated/helmChart-cert-manager-issuers.yaml b/applications/wg-easy/cert-manager-issuers/replicated/helmChart-cert-manager-issuers.yaml new file mode 100644 index 00000000..9b92a27c --- /dev/null +++ b/applications/wg-easy/cert-manager-issuers/replicated/helmChart-cert-manager-issuers.yaml @@ -0,0 +1,12 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: cert-manager-issuers +spec: + chart: + name: cert-manager-issuers + weight: 1 + helmUpgradeFlags: + - --wait + namespace: cert-manager + builder: {} diff --git a/applications/wg-easy/cert-manager-issuers/templates/issuers.yaml b/applications/wg-easy/cert-manager-issuers/templates/issuers.yaml new file mode 100644 index 00000000..898c10f9 --- /dev/null +++ b/applications/wg-easy/cert-manager-issuers/templates/issuers.yaml @@ -0,0 +1,53 @@ +{{- if .Values.local.letsencrypt.staging }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + acme: + email: {{ .Values.local.letsencrypt.email }} + server: https://acme-staging-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-staging-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-token + key: token +{{- end }} +{{- if .Values.local.letsencrypt.production }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + acme: + email: {{ .Values.local.letsencrypt.email }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-production-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-token + key: token +{{- end }} +{{- if .Values.local.letsencrypt.selfSigned }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-cluster-issuer + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + selfSigned: {} +{{- end }} diff --git a/applications/wg-easy/cert-manager-issuers/values.yaml b/applications/wg-easy/cert-manager-issuers/values.yaml new file mode 100644 index 00000000..b0ab9dd7 --- /dev/null +++ b/applications/wg-easy/cert-manager-issuers/values.yaml @@ -0,0 +1,7 @@ +local: + letsencrypt: + production: false + staging: false + selfSigned: true + email: admin@example.com + acme_host: 'dns.example.com' diff --git a/applications/wg-easy/cert-manager/Chart.yaml b/applications/wg-easy/cert-manager/Chart.yaml new file mode 100644 index 00000000..f2d64821 --- /dev/null +++ b/applications/wg-easy/cert-manager/Chart.yaml @@ -0,0 +1,10 @@ +name: cert-manager +apiVersion: v2 +version: 1.0.0 +dependencies: + - name: cert-manager + version: '1.14.5' + repository: https://charts.jetstack.io + - name: templates + version: '*' + repository: file://../charts/templates diff --git a/applications/wg-easy/cert-manager/replicated/helmChart-cert-manager.yaml b/applications/wg-easy/cert-manager/replicated/helmChart-cert-manager.yaml new file mode 100644 index 00000000..94ebeb26 --- /dev/null +++ b/applications/wg-easy/cert-manager/replicated/helmChart-cert-manager.yaml @@ -0,0 +1,12 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: cert-manager +spec: + chart: + name: cert-manager + weight: 0 + helmUpgradeFlags: + - --wait + namespace: cert-manager + builder: {} diff --git a/applications/wg-easy/cert-manager/templates/issuers.yaml b/applications/wg-easy/cert-manager/templates/issuers.yaml new file mode 100644 index 00000000..898c10f9 --- /dev/null +++ b/applications/wg-easy/cert-manager/templates/issuers.yaml @@ -0,0 +1,53 @@ +{{- if .Values.local.letsencrypt.staging }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + acme: + email: {{ .Values.local.letsencrypt.email }} + server: https://acme-staging-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-staging-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-token + key: token +{{- end }} +{{- if .Values.local.letsencrypt.production }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + acme: + email: {{ .Values.local.letsencrypt.email }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-production-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-token + key: token +{{- end }} +{{- if .Values.local.letsencrypt.selfSigned }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-cluster-issuer + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + selfSigned: {} +{{- end }} diff --git a/applications/wg-easy/cert-manager/values.yaml b/applications/wg-easy/cert-manager/values.yaml new file mode 100644 index 00000000..60c98f78 --- /dev/null +++ b/applications/wg-easy/cert-manager/values.yaml @@ -0,0 +1,30 @@ +cert-manager: + global: + leaderElection: + # Override the namespace used to store the ConfigMap for leader election + namespace: "cert-manager" + installCRDs: true + extraArgs: + - --cluster-resource-namespace=cert-manager + - --enable-certificate-owner-ref=true + resources: + requests: + cpu: 5m + memory: 45Mi + webhook: + resources: + requests: + cpu: 5m + memory: 22Mi + cainjector: + resources: + requests: + cpu: 5m + memory: 101Mi +local: + letsencrypt: + production: false + staging: false + selfSigned: false + email: admin@example.com + acme_host: 'dns.example.com' diff --git a/applications/wg-easy/charts/templates/Chart.yaml b/applications/wg-easy/charts/templates/Chart.yaml new file mode 100644 index 00000000..ff801ee9 --- /dev/null +++ b/applications/wg-easy/charts/templates/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: latest +description: Common templates +name: templates +version: 1.0.0 +kubeVersion: ">=1.16.0-0" diff --git a/applications/wg-easy/charts/templates/templates/traefik-route-tcp.yaml b/applications/wg-easy/charts/templates/templates/traefik-route-tcp.yaml new file mode 100644 index 00000000..3a47c8f8 --- /dev/null +++ b/applications/wg-easy/charts/templates/templates/traefik-route-tcp.yaml @@ -0,0 +1,21 @@ +{{- range .Values.traefikRouteTCP }} +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + {{- if .routeName }} + name: {{ .routeName }} + {{- else }} + name: {{ .serviceName }} + {{- end }} +spec: + entryPoints: + {{- range .entryPoints }} + - "{{ . }}" + {{- end }} + routes: + - match: HostSNI(`*`) + services: + - name: {{ .serviceName }} + port: {{ .servicePort }} +{{- end }} diff --git a/applications/wg-easy/charts/templates/templates/traefik-routes.yaml b/applications/wg-easy/charts/templates/templates/traefik-routes.yaml new file mode 100644 index 00000000..17dfa122 --- /dev/null +++ b/applications/wg-easy/charts/templates/templates/traefik-routes.yaml @@ -0,0 +1,106 @@ +{{- define "authUrl" -}} + {{- $auth := .auth | default dict -}} + {{- $host := default "auth" $auth.host -}} + {{- printf "http://keycloak-service.keycloak.svc:8080/realms/%s/protocol/openid-connect/certs" $host -}} +{{- end -}} + +{{- define "getMiddlewareName" -}} + {{- $host := .host | splitList "." -}} + {{- $firstPart := index $host 0 -}} + {{- $path := .path | default "" | trimPrefix "/" | replace "/" "-" -}} + {{- if $path -}} + {{- printf "%s-%s" $firstPart $path -}} + {{- else -}} + {{- $firstPart -}} + {{- end -}} +{{- end -}} + +{{- range $host, $routeConfig := .Values.traefikRoutes }} +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ $host | splitList "." | first }} +spec: + entryPoints: + - websecure + routes: + {{- range $route := $routeConfig.routes | default (list (dict "path" "")) }} + - kind: Rule + match: Host(`{{ $routeConfig.hostName | default $host }}`){{ if .path }} && Path(`{{ .path }}`){{ end }}{{ if .pathPrefix }} && PathPrefix(`{{ .pathPrefix }}`){{ end }} + {{- if or .redirectPath .authGroups }} + middlewares: + {{- if .redirectPath }} + - name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-redirect + {{- end }} + {{- if .authGroups }} + - name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-auth + - name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-inject-groups + - name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-authz + {{- end }} + {{- end }} + services: + {{- if .redirectPath }} + - kind: TraefikService + name: noop@internal + {{- else }} + - kind: {{ $routeConfig.serviceType | default "Service" }} + name: {{ $routeConfig.serviceName }} + {{- if not $routeConfig.serviceType }} + port: {{ $routeConfig.servicePort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +{{- range $host, $routeConfig := .Values.traefikRoutes }} + {{- range $route := $routeConfig.routes }} + {{- if .redirectPath }} +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-redirect +spec: + redirectRegex: + regex: ^https?://{{ $routeConfig.hostName | default $host | replace "." "\\." }}{{ or .path .pathPrefix | default "/" | replace "/" "\\/" }}/?$ + replacement: https://{{ $routeConfig.hostName | default $host }}{{ .redirectPath }} + permanent: true + {{- end }} + + {{- if .authGroups }} +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-auth +spec: + forwardAuth: + address: http://oauth2-proxy.oauth2-proxy.svc.cluster.local:80 + trustForwardHeader: true + authResponseHeaders: + - Authorization +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-inject-groups +spec: + headers: + customRequestHeaders: + X-Required-Groups: {{ join "," .authGroups | quote }} +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "getMiddlewareName" (dict "host" $host "path" (or .path .pathPrefix)) }}-authz +spec: + plugin: + jwt: + OpaUrl: http://opa.opa.svc:8181/v1/data/keycloak/authz + Required: true + Keys: + - {{ include "authUrl" (dict "auth" $routeConfig.auth) }} + {{- end }} + {{- end }} +{{- end }} diff --git a/applications/wg-easy/charts/templates/values.yaml b/applications/wg-easy/charts/templates/values.yaml new file mode 100644 index 00000000..9340364a --- /dev/null +++ b/applications/wg-easy/charts/templates/values.yaml @@ -0,0 +1,27 @@ +#traefikRoutes: +# host.example.com: +# serviceName: my-serviceName +# servicePort: my-servicePort +# #Optional: Override the auth settings +# #auth: +# # domain: example.com +# # host: auth +# # realm: example.com +# routes: +# - path: / +# - path: /login +# auth: true +# - path: /admin +# auth: true +# authGroups: +# - admin +# - superuser +# - path: /old-path +# redirectPath: /new-path +# - pathPrefix: /docs +# auth: true +# traefikRouteTCP: +# - serviceName: +# servicePort: +# entryPoints: +# - diff --git a/applications/wg-easy/doc-todo-list.md b/applications/wg-easy/doc-todo-list.md new file mode 100644 index 00000000..b3201a48 --- /dev/null +++ b/applications/wg-easy/doc-todo-list.md @@ -0,0 +1,134 @@ +# WG-Easy Helm Chart Pattern Documentation Plan + +This document outlines specific work items for implementing the updated documentation strategy for the WG-Easy Helm Chart development pattern. + +## General Implementation Guidelines + +- [x] Use GitHub Markdown syntax for all documents +- [x] Test all documents in GitHub preview to ensure proper rendering +- [x] Use relative links for cross-document references +- [x] Maintain consistent terminology across all documents +- [x] Apply appropriate heading levels (h1 for document title, h2 for major sections) +- [x] Include code blocks with proper language specification +- [x] Use GitHub-specific features where appropriate (collapsible sections, task lists, tables) + +## Image Creation + +- [x] Create Architecture Diagram showing component relationships + - Include chart wrapping concept + - Show relationship between charts and templates + - Visualize dependency flow + +- [x] Create Workflow Diagram illustrating progressive complexity + - Show the 7 stages of development + - Highlight validation points + - Indicate approximate time requirements for each step + +- [x] Create Configuration Flow Diagram showing modular configuration + - Illustrate how per-chart configs are merged + - Show release preparation flow + - Include chart dependency relationships + +## README.md + +- [x] Rewrite introduction to focus on core principles and pattern benefits +- [x] Replace testing emphasis with progressive development workflow +- [x] Add placeholder for Architecture Diagram +- [x] Create concise overview of repository structure +- [x] Add clear explanation of each core principle with brief examples: + - Progressive Complexity + - Fast Feedback Loops + - Reproducible Steps + - Modular Configuration + - Automation First +- [x] Add links to supporting documentation +- [x] REMOVE: Detailed installation instructions +- [x] REMOVE: Extensive testing methodology +- [x] REMOVE: Exhaustive workflow details + +## docs/chart-structure.md + +- [x] Focus content on explaining the modular chart approach +- [x] Add detailed explanation of chart wrapping benefits +- [x] Improve directory structure explanation with visual aids +- [x] Add examples of how charts are composed +- [x] Explain shared templates concept with examples +- [x] Describe how modular configuration works +- [x] Include diagram placeholder +- [x] REMOVE: "Best Practices" section +- [x] REMOVE: Overly detailed examples not illustrating the pattern + +## docs/development-workflow.md (NEW - replacing getting-started.md) + +- [x] Create new document focused on workflow stages +- [x] Add clear introduction explaining progressive complexity approach +- [x] Detail each workflow stage with examples: + 1. Defining chart dependencies and verification + 2. Configuring with values.yaml and templates + 3. Local validation with helm template + 4. Single chart install/uninstall + 5. Integration testing with helmfile + 6. Release preparation + 7. Embedded cluster testing +- [x] Include specific command examples for each stage +- [x] Add explanation of how each stage supports fast feedback +- [x] Include workflow diagram placeholder +- [x] Explain when to move from one stage to the next +- [x] REMOVE FROM OLD DOC: Detailed installation instructions +- [x] REMOVE FROM OLD DOC: Peripheral topics not related to workflow + +## docs/task-reference.md (Replacing tasks.md) + +- [x] Reorganize tasks by purpose (development, release, testing) +- [x] Add brief description for each task focusing on what it accomplishes +- [x] Include examples of common task combinations +- [x] Explain how tasks support the automation-first principle +- [x] Group related tasks together +- [x] Add cross-references to development workflow stages +- [x] REMOVE: Verbose command details +- [x] REMOVE: Installation instructions +- [x] REMOVE: Extensive options documentation duplicating Taskfile.yaml + +## docs/replicated-integration.md + +- [x] Condense to focus on integration with overall pattern +- [x] Add brief overview of Replicated's role +- [x] Explain modular configuration with Replicated +- [x] Describe key integration points in the workflow +- [x] Add references to official Replicated documentation +- [x] Integrate with workflow stages where appropriate +- [x] REMOVE: "Best Practices" section +- [x] REMOVE: Detailed configuration examples +- [x] REMOVE: Content duplicating Replicated's documentation + +## Documents to Remove Entirely + +- [x] Remove docs/testing.md + - Extract any relevant concepts and merge into main documents if applicable + +## Examples to Include + +- [x] Progressive Complexity Example: + - Complete workflow from single chart to release + - Validation points at each stage + - Issue isolation demonstration + +- [x] Modular Configuration Example: + - Per-chart config.yaml maintenance + - Config merging during release preparation + - Team ownership benefits + +- [x] Chart Wrapping Example: + - Simple chart wrapping upstream chart + - Benefits explanation + - Environment consistency demonstration + +## Final Review Checklist + +- [x] Ensure documentation is beginner-friendly but valuable for experienced users +- [x] Verify all links work correctly +- [x] Check that all GitHub Markdown renders correctly +- [x] Confirm core principles are consistently emphasized +- [x] Validate that examples are clear and illustrative +- [x] Ensure removed content doesn't result in information gaps +- [x] Check for consistent terminology throughout diff --git a/applications/wg-easy/docs/chart-structure.md b/applications/wg-easy/docs/chart-structure.md new file mode 100644 index 00000000..d4052896 --- /dev/null +++ b/applications/wg-easy/docs/chart-structure.md @@ -0,0 +1,132 @@ +# Chart Structure Guide + +This document explains the modular chart approach used in the WG-Easy Helm chart pattern. + +## Modular Chart Architecture + +![Chart Structure](architecture.png) + +The WG-Easy pattern is built around a modular approach to Helm charts, where upstream charts are wrapped in local charts and enhanced with shared templates and customizations. + +### Directory Structure + +``` +applications/wg-easy/ +├── charts/templates/ # Common templates shared across charts +│ ├── traefik-routes.yaml # Templates for Traefik IngressRoutes +│ └── traefik-route-tcp.yaml # Templates for Traefik TCP routes +├── cert-manager/ # Wrapped cert-manager chart +├── cert-manager-issuers/ # Chart for cert-manager issuers +├── replicated/ # Root Replicated configuration +├── replicated-sdk/ # Replicated SDK chart +├── traefik/ # Wrapped Traefik chart +├── wg-easy/ # Main application chart +├── helmfile.yaml # Defines chart installation order +└── Taskfile.yaml # Main task definitions +``` + +## Chart Wrapping Concept + +Chart wrapping is a core technique in this pattern where upstream Helm charts are encapsulated in local charts rather than used directly. This provides several key benefits: + +### Example: +```yaml +# cert-manager/Chart.yaml +apiVersion: v2 +name: cert-manager +version: 1.0.0 +dependencies: + - name: cert-manager + version: '1.14.5' + repository: https://charts.jetstack.io + - name: templates + version: '*' + repository: file://../charts/templates +``` + +### Customization Control + +Wrapper charts allow you to extend or modify the upstream chart's behavior: + +1. **Custom Templates**: Add your own Kubernetes resources alongside the upstream chart +2. **Value Overrides**: Set defaults appropriate for your environment +3. **Additional Resources**: Include related resources that complement the main chart + +### Version Management + +Wrapped charts give you precise control over dependency versions: + +1. **Version Pinning**: Lock dependencies to specific versions for stability +2. **Upgrade Management**: Test upgrades in isolation before promoting to production +3. **Compatibility Assurance**: Ensure all components work together + +## Shared Templates + +The pattern uses a shared templates chart (`charts/templates/`) that contains reusable components: + +```yaml +# From the wg-easy/values.yaml file showing templates usage +templates: + traefikRoutes: + web: + enabled: true + entryPoint: web + rule: "Host(`{{ .Values.wireguard.host }}`)" + service: wg-easy + port: 51821 +``` + +This approach provides: + +1. **Consistency**: Standard implementations across charts +2. **Maintainability**: Single source of truth for common patterns +3. **Simplicity**: Easy reuse of complex configurations + +## Chart Composition + +The charts are composed together using Helmfile, which manages dependencies and installation order: + +```yaml +# Example from helmfile.yaml +releases: + - name: cert-manager + namespace: cert-manager + chart: ./cert-manager + createNamespace: true + wait: true + + - name: cert-manager-issuers + namespace: cert-manager + chart: ./cert-manager-issuers + createNamespace: true + wait: true + needs: + - cert-manager/cert-manager +``` + +This ensures that charts are installed in the correct order with proper dependencies. + +## Modular Configuration + +Each chart can define its own configuration that is merged during release preparation: + +``` +traefik/ +├── values.yaml # Default chart values +└── replicated/ + └── config.yaml # Traefik-specific configuration + └── helmChart-traefik.yaml # Installation instructions + +wg-easy/ +├── values.yaml # Default chart values +└── replicated/ + └── config.yaml # WG-Easy-specific configuration + └── helmChart-wg-easy.yaml # Installation instructions +``` + +Benefits of this approach: + +1. **Team Ownership**: Different teams can own their component configurations +2. **Clear Boundaries**: Separation of concerns between components +3. **Simplified Maintenance**: Changes to one component don't affect others +4. **Automatic Merging**: During release, all configs are combined into a single file diff --git a/applications/wg-easy/docs/development-workflow.md b/applications/wg-easy/docs/development-workflow.md new file mode 100644 index 00000000..a457942b --- /dev/null +++ b/applications/wg-easy/docs/development-workflow.md @@ -0,0 +1,260 @@ +# Development Workflow + +This document outlines the progressive development workflow for the WG-Easy Helm chart pattern, guiding you through each stage from initial chart configuration to complete application deployment. + +## Progressive Complexity Approach + +The core philosophy of this workflow is to start simple and add complexity incrementally, providing fast feedback at each stage. This allows developers to: + +- Identify and fix issues early when they're easier to debug +- Get rapid feedback on changes without waiting for full deployments +- Build confidence in changes through progressive validation +- Maintain high velocity while ensuring quality + +![Workflow Diagram](workflow-diagram.png) + +## Prerequisites + +Before starting the development workflow, ensure you have the following tools installed: + +- **Task:** The task runner used in this project. ([Installation Guide](https://taskfile.dev/installation/)) +- **Replicated CLI:** For managing test clusters and application releases. ([Installation Guide](https://docs.replicated.com/reference/replicated-cli-installing)) +- **Helm:** The Kubernetes package manager. ([Installation Guide](https://helm.sh/docs/intro/install/)) +- **Helmfile:** For orchestrating Helm chart deployments. ([Installation Guide](https://github.com/helmfile/helmfile#installation)) +- **kubectl:** The Kubernetes command-line tool. ([Installation Guide](https://kubernetes.io/docs/tasks/tools/install-kubectl/)) +- **jq:** A command-line JSON processor. ([Download Page](https://stedolan.github.io/jq/download/)) +- **yq:** A command-line YAML processor. ([Installation Guide](https://github.com/mikefarah/yq#install)) +- **gcloud CLI:** Google Cloud command-line interface (optional, only required for GCP-specific tasks). ([Installation Guide](https://cloud.google.com/sdk/docs/install)) +- **Standard Unix Utilities:** `find`, `xargs`, `grep`, `awk`, `wc`, `tr`, `cp`, `mv`, `rm`, `mkdir`, `echo`, `sleep`, `test`, `eval` (typically available by default on Linux and macOS). + +## Workflow Stages + +### Stage 1: Chart Dependencies and Verification + +Begin by defining and verifying chart dependencies. + +1. Define or update dependencies in `Chart.yaml`: + + ```yaml + # Example: cert-manager/Chart.yaml + dependencies: + - name: cert-manager + version: '1.14.5' + repository: https://charts.jetstack.io + - name: templates + version: '*' + repository: file://../charts/templates + ``` + +2. Update dependencies: + + ```bash + task dependencies-update + # Or for a single chart: + helm dependency update ./cert-manager + ``` + +3. Verify charts were downloaded: + + ```bash + ls -la ./cert-manager/charts/ + ``` + +**Validation point**: Dependencies should be successfully downloaded to the `/charts` directory. + +### Stage 2: Configuration with values.yaml and Templates + +Configure chart values and create or modify templates. + +1. Update the chart's `values.yaml` with appropriate defaults: + + ```yaml + # Example: traefik/values.yaml + certs: + selfSigned: true + traefik: + service: + type: NodePort + ports: + web: + nodePort: 80 + ``` + +2. Create or customize templates: + + ```yaml + # Example: Adding a template for TLS configuration + apiVersion: traefik.containo.us/v1alpha1 + kind: TLSOption + metadata: + name: default + namespace: traefik + spec: + minVersion: VersionTLS12 + ``` + +**Validation point**: Configuration values are properly defined and templates are syntactically correct. + +### Stage 3: Local Validation with helm template + +Validate chart templates locally without deploying to a cluster. + +1. Run helm template to render the chart and inspect manifests: + ```bash + helm template ./cert-manager | less + ``` + +**Validation point**: Generated Kubernetes manifests should be valid and contain the expected resources. + +### Stage 4: Single Chart Install/Uninstall + +Deploy individual charts to a test cluster to verify functionality. + +1. Create a test cluster if needed: + + ```bash + task cluster-create + task setup-kubeconfig + ``` + +2. Install a single chart: + + ```bash + helm install cert-manager ./cert-manager -n cert-manager --create-namespace + ``` + +3. Verify the deployment: + + ```bash + kubectl get pods -n cert-manager + ``` + +4. Test chart functionality: + + ```bash + # Example: Test cert-manager with a test certificate + kubectl apply -f ./some-test-certificate.yaml + kubectl get certificate -A + ``` + +5. Uninstall when done or making changes and repeat step 2: + + ```bash + helm uninstall cert-manager -n cert-manager + ``` + +**Validation point**: Chart should deploy successfully and function as expected. + +### Stage 5: Integration Testing with helmfile + +Test multiple charts working together using Helmfile orchestration. + +1. Ensure helmfile.yaml is configured with the correct dependencies: + + ```yaml + releases: + - name: cert-manager + namespace: cert-manager + chart: ./cert-manager + # ... + - name: cert-manager-issuers + namespace: cert-manager + chart: ./cert-manager-issuers + # ... + needs: + - cert-manager/cert-manager + ``` + +2. Deploy all charts: + + ```bash + task helm-deploy + ``` + +3. Verify cross-component integration: + + ```bash + # Check if issuers are correctly using cert-manager + kubectl get clusterissuers + kubectl get issuers -A + + # Verify Traefik routes + kubectl get ingressroutes -A + ``` + +**Validation point**: All components should deploy in the correct order and work together. + +### Stage 6: Release Preparation + +Prepare a release package for distribution. + +1. Generate release files: + + ```bash + task release-prepare + ``` + +2. Inspect the generated release files: + + ```bash + ls -la ./release/ + ``` + +3. Create a release: + + ```bash + task release-create + ``` + +**Validation point**: Release files should be correctly generated and the release should be created successfully. + +### Stage 7: Test installing a helm based release + +TODO: Finish implementing helm based release testing + +### Stage 8: Embedded Cluster Testing + +Test the full application and configuration screen in a using an embedded cluster. + +1. Create a VM for testing: + + ```bash + task gcp-vm-create + ``` + +2. Set up an embedded cluster: + + ```bash + task embedded-cluster-setup + ``` + +3. Verify the application: + + ```bash + # Test accessing the application frontend + echo "Application URL: https://" + ``` + +**Validation point**: Verify configuration workflow, preflight, and other Embedded Cluster configurations. + +## Moving Between Stages + +The workflow is designed to be iterative. Here are guidelines for when to move from one stage to the next: + +- **Move forward** when the current stage validation passes without issues +- **Move backward** when problems are detected to diagnose and fix at a simpler level +- **Stay in a stage** if you're making changes focused on that particular level of complexity + +## Fast Feedback Examples + +Each stage provides fast feedback: + +1. **Chart Dependencies**: Immediate feedback on dependency resolution (seconds) +2. **Values Configuration**: Immediate feedback on configuration structure (seconds) +3. **Template Validation**: Fast feedback on template rendering (seconds) +4. **Single Chart Install**: Quick feedback on chart functionality (1-2 minutes) +5. **Integration Testing**: Feedback on component interaction (5-10 minutes) +6. **Release Preparation**: Feedback on release packaging (seconds) +7. **Embedded Testing**: Full system feedback (10-15 minutes) + +This progressive approach allows you to catch and fix issues at the earliest possible stage, minimizing the time spent debugging complex problems in fully deployed environments. diff --git a/applications/wg-easy/docs/examples.md b/applications/wg-easy/docs/examples.md new file mode 100644 index 00000000..301753bc --- /dev/null +++ b/applications/wg-easy/docs/examples.md @@ -0,0 +1,228 @@ +# WG-Easy Development Pattern Examples + +This document provides practical examples of the key concepts in the WG-Easy Helm chart pattern. + +## Progressive Complexity Example + +This example demonstrates the complete workflow from working with a single chart to creating a full release. + +### Stage 1-2: Chart Dependencies and Configuration + +```yaml +# cert-manager/Chart.yaml +apiVersion: v2 +name: cert-manager +version: 1.0.0 +dependencies: + - name: cert-manager + version: '1.14.5' + repository: https://charts.jetstack.io + - name: templates + version: '*' + repository: file://../charts/templates +``` + +```bash +# Update dependencies +helm dependency update ./cert-manager + +# Verify dependencies were downloaded +ls -la ./cert-manager/charts/ +``` + +### Stage 3: Local Validation + +```bash +# Render templates locally to verify +helm template ./cert-manager --output-dir ./rendered-templates + +# Verify the output +ls -la ./rendered-templates/cert-manager/templates/ +``` + +### Stage 4: Single Chart Install + +```bash +# Create a test cluster +task cluster-create +task setup-kubeconfig + +# Install the single chart +helm install cert-manager ./cert-manager -n cert-manager --create-namespace + +# Validate deployment +kubectl get pods -n cert-manager +``` + +### Stage 5: Integration Testing + +```bash +# Deploy multiple charts using helmfile +task deploy-helm + +# Verify integration points +kubectl get clusterissuers +kubectl get ingressroutes -A +``` + +### Stage 6-7: Release Preparation and Testing + +```bash +# Prepare release files +task release-prepare + +# Create a release +task release-create + +# Test in embedded environment +task create-gcp-vm +task setup-embedded-cluster +``` + +### Validation Points + +The example demonstrates validation at each stage: + +1. **Dependencies**: Verify charts are downloaded correctly +2. **Template Rendering**: Ensure templates produce valid Kubernetes resources +3. **Single Chart**: Confirm chart installs and runs properly in isolation +4. **Integration**: Validate charts work together as expected +5. **Release**: Verify proper packaging and release creation +6. **Production Environment**: Confirm full system functionality + +### Issue Isolation Example + +If an issue is discovered during integration testing (Stage 5): + +1. **Identify Components**: Determine which charts are involved +2. **Scale Back**: Return to Stage 4 to test each chart individually +3. **Find Root Cause**: Isolate the specific chart or interaction causing the issue +4. **Fix and Validate**: Make changes and validate at the simpler level first +5. **Progress Again**: Move back through the stages to ensure the fix works in the full system + +## Modular Configuration Example + +This example shows how per-chart configuration is maintained and merged during release. + +### Per-Chart Configuration + +```yaml +# traefik/replicated/config.yaml +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: not-used +spec: + groups: + - name: traefik-config + title: Traefik + items: + - name: domain + title: Domain + type: text + required: true +``` + +```yaml +# wg-easy/replicated/config.yaml +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: not-used +spec: + groups: + - name: wireguard-config + title: Wireguard + items: + - name: password + title: Admin password + type: password + required: true +``` + +### Configuration Merging + +During release preparation, these files are automatically merged: + +```bash +task release-prepare +``` + +This produces a combined `config.yaml` in the release directory with all options from both components. + +### Team Ownership Benefits + +This approach allows: + +1. **Distributed Responsibilities**: The Traefik team manages Traefik options, WG-Easy team manages WG-Easy options +2. **Independent Updates**: Teams can update their configuration without coordinating changes +3. **Clear Boundaries**: Configuration ownership follows component ownership +4. **Reduced Conflicts**: Fewer merge conflicts in version control + +## Chart Wrapping Example + +This example demonstrates wrapping an upstream chart and the benefits it provides. + +### Simple Chart Wrapping + +```yaml +# traefik/Chart.yaml - Wrapper chart +apiVersion: v2 +name: traefik +version: 1.0.0 +dependencies: + - name: traefik + version: '28.0.0' + repository: https://helm.traefik.io/traefik + - name: templates + version: '*' + repository: file://../charts/templates +``` + +```yaml +# traefik/values.yaml - Our custom defaults +traefik: + service: + type: NodePort + ports: + web: + nodePort: 80 + redirectTo: websecure + websecure: + nodePort: 443 +``` + +### Custom Templates + +```yaml +# traefik/templates/certificate.yaml - Custom resource +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: traefik-cert + namespace: traefik +spec: + secretName: traefik-cert + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + {{- range .Values.certs.dnsNames }} + - {{ . | quote }} + {{- end }} +``` + +### Benefits + +1. **Extended Functionality**: Add custom resources that aren't part of the upstream chart +2. **Version Management**: Control which version of the upstream chart is used +3. **Simplified Updates**: Test upstream chart updates in isolation +4. **Custom Defaults**: Set defaults that match your use case + +### Environment Consistency + +The wrapper chart ensures consistency by: + +1. **Shared Templates**: Reuse common patterns +2. **Explicit Dependencies**: Clear relationship between components +3. **Controlled Customization**: Customizations applied in a standard way diff --git a/applications/wg-easy/docs/replicated-integration.md b/applications/wg-easy/docs/replicated-integration.md new file mode 100644 index 00000000..acba7614 --- /dev/null +++ b/applications/wg-easy/docs/replicated-integration.md @@ -0,0 +1,141 @@ +# Replicated Integration + +This document explains how the WG-Easy Helm chart pattern integrates with the Replicated platform, focusing on key integration points and modular configuration. + +## Replicated Overview + +Replicated is a platform that enables software vendors to deliver and manage Kubernetes applications to enterprise customers. In the context of this development pattern, Replicated provides: + +1. **Distribution**: Package and ship your application securely +2. **Configuration Management**: User-friendly configuration interfaces +3. **Licensing**: Control access through license files +4. **Embedded Kubernetes**: Deploy to environments without existing clusters +5. **Updates**: Manage application versions and updates + +## Integration Points + +The WG-Easy pattern integrates with Replicated at the following key points in the development workflow: + +### 1. Chart Structure Integration + +Each chart includes a `replicated` directory containing: + +``` +cert-manager/replicated/ +├── helmChart-cert-manager.yaml # Installation instructions +└── config.yaml # Component-specific configuration +``` + +### 2. Modular Configuration + +Each chart can define its own `config.yaml` with component-specific configuration options: + +```yaml +# Example: wg-easy/replicated/config.yaml +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: not-used +spec: + groups: + - name: wireguard-config + title: Wireguard + description: Wireguard configuration + items: + - name: password + title: Admin password + type: password + required: true + - name: domain + title: IP or domain + help_text: Domain or IP which the vpn is accessible on + type: text + required: true +``` + +During release preparation, these individual `config.yaml` files are automatically merged into a single configuration file. + +### 3. Chart Installation + +The `helmChart-*.yaml` files define how each Helm chart is installed by Replicated installers: + +```yaml +# Example: traefik/replicated/helmChart-traefik.yaml +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: traefik +spec: + chart: + name: traefik + chartVersion: '1.0.0' + weight: 2 + helmUpgradeFlags: + - --wait + namespace: traefik + values: + traefik: + ports: + web: + nodePort: 80 + websecure: + nodePort: 443 +``` + +### 4. Release Workflow + +The integration with Replicated's release process is handled by the `release-prepare` and `release-create` tasks: + +```bash +# Prepare release files +task release-prepare + +# Create and promote a release +task release-create CHANNEL=Beta +``` + +The `release-prepare` task: + +1. Copies all Replicated YAML files to a release directory +2. Merges all `config.yaml` files from different components +3. Packages all Helm charts + +## Modular Configuration Benefits + +This modular configuration approach provides several benefits: + +1. **Team Ownership**: Different teams can own their component's configuration +2. **Isolation**: Changes to one component's configuration don't affect others +3. **Simplified Development**: Focus on your component without worrying about the full configuration +4. **Automatic Merging**: Configuration merging is automated at release time + +## Embedded Cluster Support + +Replicated's embedded Kubernetes capability is configured via the `cluster.yaml` file: + +```yaml +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: 2.1.3+k8s-1.29 + unsupportedOverrides: + k0s: |- + config: + spec: + workerProfiles: + - name: default + values: + allowedUnsafeSysctls: + - net.ipv4.ip_forward +``` + +This enables running the application in environments without a pre-existing Kubernetes cluster. + +## Further Replicated Documentation + +For detailed information about Replicated's capabilities and options, refer to the official documentation: + +- [Replicated KOTS Documentation](https://docs.replicated.com/reference/kots-cli) +- [Helm Chart Integration](https://docs.replicated.com/vendor/helm-installing) +- [Configuration Options](https://docs.replicated.com/vendor/config-screen) +- [Embedded Clusters](https://docs.replicated.com/vendor/embedded-clusters) diff --git a/applications/wg-easy/docs/task-reference.md b/applications/wg-easy/docs/task-reference.md new file mode 100644 index 00000000..f28267b6 --- /dev/null +++ b/applications/wg-easy/docs/task-reference.md @@ -0,0 +1,131 @@ +# Task Reference + +This document provides a concise reference for the tasks available in the WG-Easy Helm chart development pattern, organized by their purpose in the development workflow. + +## Development Tasks + +These tasks support the iterative development process, focusing on fast feedback loops. + +| Task | Description | Related Workflow Stage | +|------|-------------|------------------------| +| `dependencies-update` | Updates Helm dependencies for all charts in the repository | Stage 1: Dependencies | +| `helm-deploy` | Deploys all charts using helmfile with proper sequencing | Stage 5: Integration Testing | +| `ports-expose` | Exposes the configured ports on the cluster for testing | Stage 4-5: Chart Installation/Integration | +| `remove-k3s-traefik` | Removes pre-installed Traefik from k3s clusters to avoid conflicts | Stage 4-5: Chart Installation/Integration | + +### Common Development Combinations + +**Complete Update and Deploy:** +```bash +task update-dependencies && task deploy-helm +``` + +**Single Chart Testing:** +```bash +helm dependency update ./traefik +helm install traefik ./traefik -n traefik --create-namespace +``` + +## Environment Tasks + +These tasks help manage the development and testing environments. + +| Task | Description | Related Workflow Stage | +|------|-------------|------------------------| +| `cluster-create` | Creates a test Kubernetes cluster using Replicated's Compatibility Matrix | Stage 4: Single Chart Install | +| `setup-kubeconfig` | Retrieves and sets up the kubeconfig for the test cluster | Stage 4: Single Chart Install | +| `delete-cluster` | Deletes the test cluster and cleans up resources | Stage 4-5: Cleanup | +| `create-gcp-vm` | Creates a GCP VM instance for embedded cluster testing | Stage 7: Embedded Testing | +| `delete-gcp-vm` | Deletes the GCP VM instance after testing | Stage 7: Cleanup | +| `setup-embedded-cluster` | Sets up a Replicated embedded cluster on the GCP VM | Stage 7: Embedded Testing | +| `list-cluster` | List the test cluster with the cluster id, name, and expiration date | Stage 4: Single Chart Install | +| `verify-kubeconfig` | Verifies the kubeconfig for the test cluster and removes the cluster if it is expired | Stage 4: Single Chart Install | +| `cluster-delete` | Deletes the test cluster and cleans up resources | Stage 4-5: Cleanup | +| `gcp-vm-create` | Creates a GCP VM instance for embedded cluster testing | Stage 7: Embedded Testing | +| `gcp-vm-delete` | Deletes the GCP VM instance after testing | Stage 7: Cleanup | +| `embedded-cluster-setup` | Sets up a Replicated embedded cluster on the GCP VM | Stage 7: Embedded Testing | + +### Common Environment Combinations + +**Create Test Environment:** +```bash +task cluster-create && task setup-kubeconfig +OR +task setup-kubeconfig +``` + +While tasks can be run in order, they also have dependencies. Running the get-kubeconfig task for example will also create a cluster if the test cluster hasn't been created already. + +**Cleanup After Testing:** +```bash +task cluster-delete +``` + +Creating a cluster can take up to 5 minutes and helm charts should be uninstalled/reinstalled while developing rather than removing the entire cluster to iterate in seconds rather than minutes. + +## Release Tasks + +These tasks support preparing and creating releases. + +| Task | Description | Related Workflow Stage | +|------|-------------|------------------------| +| `release-prepare` | Packages charts and merges configuration files for release | Stage 6: Release Preparation | +| `release-create` | Creates and promotes a release using the Replicated CLI | Stage 6: Release Preparation | +| `test` | Runs basic validation tests against the deployed application | Stage 5-7: Validation | + +TODO: The test task is a placeholder currently it just sleeps and returns positive. + +### Release Process Example + +```bash +task release-prepare && task release-create CHANNEL=Beta +``` + +## Automation Tasks + +These tasks provide end-to-end automation combining multiple individual tasks. + +| Task | Description | Related Workflow Stages | +|------|-------------|-------------------------| +| `full-test-cycle` | Runs a complete test cycle from cluster creation to deletion | Stages 4-5: Full Testing | + +This task performs the following sequence: + +1. Creates a cluster +2. Sets up the kubeconfig +3. Exposes ports +4. Removes pre-installed Traefik +5. Updates dependencies +6. Deploys all charts +7. Runs tests +8. Deletes the cluster + +## Task Parameters + +Many tasks accept parameters to customize their behavior. Here are the most commonly used parameters: + +| Parameter | Used With | Description | Default | +|-----------|-----------|-------------|---------| +| `CLUSTER_NAME` | `cluster-create`, `setup-kubeconfig` | Name for the cluster | "test-cluster" | +| `K8S_VERSION` | `cluster-create` | Kubernetes version | "1.32.2" | +| `DISTRIBUTION` | `cluster-create` | Cluster distribution | "k3s" | +| `CHANNEL` | `release-create` | Channel to promote to | "Unstable" | +| `RELEASE_NOTES` | `release-create` | Notes for the release | "" | +| `GCP_PROJECT` | `gcp-vm-create` | GCP project ID | Required | +| `GCP_ZONE` | `gcp-vm-create` | GCP zone | "us-central1-a" | + +Parameters in the Taskfile.yaml try to always have defaults so that it works out of the box but allows customization for common values. + +## Supporting the Development Workflow + +These tasks are designed to support the progressive complexity approach: + +1. **Early Stages** - Use `dependencies-update` and helm commands directly +2. **Middle Stages** - Use `cluster-create`, `helm-deploy`, and `test` +3. **Later Stages** - Use `release-prepare`, `release-create`, and embedded cluster tasks + +This organization allows developers to focus on the appropriate level of complexity at each stage of development. + +## Cross-References to Workflow Stages + +Refer to the [Development Workflow](development-workflow.md) document for details on each stage and when to use specific tasks. diff --git a/applications/wg-easy/helmfile.yaml b/applications/wg-easy/helmfile.yaml new file mode 100644 index 00000000..4adc18e3 --- /dev/null +++ b/applications/wg-easy/helmfile.yaml @@ -0,0 +1,100 @@ +# Global configuration +helmDefaults: + verify: false + wait: true + timeout: 600 + atomic: true + cleanupOnFail: true + +environments: + default: + values: + - chartSources: + certManager: ./cert-manager + certManagerIssuers: ./cert-manager-issuers + traefik: ./traefik + wgEasy: ./wg-easy + replicatedSDK: ./replicated-sdk + - extras: + enableReplicatedSDK: false + replicated: + values: + - app: '{{ env "APP" | default "wg-easy" }}' + - channel: '{{ env "CHANNEL" | default "Unstable" }}' + - chartSources: + certManager: 'oci://registry.replicated.com/{{ env "APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "Unstable" }}/cert-manager' + certManagerIssuers: 'oci://registry.replicated.com/{{ env "APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "Unstable" }}/cert-manager-issuers' + traefik: 'oci://registry.replicated.com/{{ env "APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "Unstable" }}/traefik' + wgEasy: 'oci://registry.replicated.com/{{ env "APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "Unstable" }}/wg-easy' + replicatedSDK: 'oci://registry.replicated.com/{{ env "APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "Unstable" }}/replicated-sdk' + - extras: + enableReplicatedSDK: true +--- +releases: + # Install cert-manager with CRDs but without issuers + - name: cert-manager + namespace: cert-manager + chart: {{ .Values.chartSources.certManager }} + createNamespace: true + wait: true + installed: true + skipDeps: true + + # Install issuers separately after cert-manager is ready + - name: cert-manager-issuers + namespace: cert-manager + chart: {{ .Values.chartSources.certManagerIssuers }} + createNamespace: true + wait: true + installed: true + skipDeps: true + needs: + - cert-manager/cert-manager + + - name: traefik + namespace: traefik + chart: {{ .Values.chartSources.traefik }} + createNamespace: true + wait: true + installed: true + skipDeps: true + needs: + - cert-manager/cert-manager-issuers + values: + - traefik: + ports: + web: + nodePort: 30080 + websecure: + nodePort: 30443 + + # Install replicated-sdk (only in replicated environment) + - name: replicated-sdk + namespace: replicated-sdk + chart: {{ .Values.chartSources.replicatedSDK }} + createNamespace: true + wait: true + installed: {{ .Values.extras.enableReplicatedSDK }} + skipDeps: true + needs: + - traefik/traefik + + - name: wg-easy + namespace: wg-easy + chart: {{ .Values.chartSources.wgEasy }} + createNamespace: true + wait: true + installed: true + skipDeps: true + needs: + - traefik/traefik + values: + - wg-easy: + wireguard: + host: '{{ env "TF_EXPOSED_URL" }}' + - templates: + traefikRoutes: + web-tls: + hostName: '{{ env "TF_EXPOSED_URL" }}' + web: + hostName: '{{ env "TF_EXPOSED_HTTP_URL" }}' diff --git a/applications/wg-easy/replicated-sdk/Chart.yaml b/applications/wg-easy/replicated-sdk/Chart.yaml new file mode 100644 index 00000000..f933f479 --- /dev/null +++ b/applications/wg-easy/replicated-sdk/Chart.yaml @@ -0,0 +1,10 @@ +name: replicated-sdk +version: 1.0.0 +apiVersion: v2 +dependencies: + - name: templates + version: '*' + repository: file://../charts/templates + - name: replicated + repository: oci://registry.replicated.com/library + version: 1.1.1 \ No newline at end of file diff --git a/applications/wg-easy/replicated-sdk/replicated/helmChart-replicated-sdk.yaml b/applications/wg-easy/replicated-sdk/replicated/helmChart-replicated-sdk.yaml new file mode 100644 index 00000000..acbc2e60 --- /dev/null +++ b/applications/wg-easy/replicated-sdk/replicated/helmChart-replicated-sdk.yaml @@ -0,0 +1,20 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: replicated-sdk +spec: + chart: + name: replicated-sdk + weight: 1 + + # helmUpgradeFlags specifies additional flags to pass to the `helm upgrade` command. + helmUpgradeFlags: + - --skip-crds + - --timeout + - 30s + - --history-max=15 + - --wait + + values: {} + namespace: replicated-sdk + builder: {} diff --git a/applications/wg-easy/replicated-sdk/values.yaml b/applications/wg-easy/replicated-sdk/values.yaml new file mode 100644 index 00000000..32fbdcb4 --- /dev/null +++ b/applications/wg-easy/replicated-sdk/values.yaml @@ -0,0 +1 @@ +# Values for replicated-sdk chart \ No newline at end of file diff --git a/applications/wg-easy/replicated/application.yaml b/applications/wg-easy/replicated/application.yaml new file mode 100644 index 00000000..0e2484e4 --- /dev/null +++ b/applications/wg-easy/replicated/application.yaml @@ -0,0 +1,22 @@ +apiVersion: kots.io/v1beta1 +kind: Application +metadata: + name: wg-easy +spec: + title: wg-easy + icon: https://www.logo.wine/a/logo/WireGuard/WireGuard-Icon-Logo.wine.svg + #releaseNotes: These are our release notes + allowRollback: true + #additionalImages: + # - jenkins/jenkins:lts + #additionalNamespaces should be populated by the Task file + #ports: + # - serviceName: wg-easy/web + # servicePort: 51821 + # applicationUrl: "http://web" + statusInformers: + - wg-easy/deployment/public + - traefik/deployment/traefik + - cert-manager/deployment/cert-manager + - cert-manager/deployment/cert-manager-cainjector + - cert-manager/deployment/cert-manager-webhook diff --git a/applications/wg-easy/replicated/cluster.yaml b/applications/wg-easy/replicated/cluster.yaml new file mode 100644 index 00000000..7a3deb7b --- /dev/null +++ b/applications/wg-easy/replicated/cluster.yaml @@ -0,0 +1,13 @@ +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: 2.1.3+k8s-1.29 + unsupportedOverrides: + k0s: |- + config: + spec: + workerProfiles: + - name: default + values: + allowedUnsafeSysctls: + - net.ipv4.ip_forward diff --git a/applications/wg-easy/replicated/config.yaml b/applications/wg-easy/replicated/config.yaml new file mode 100644 index 00000000..d941c4be --- /dev/null +++ b/applications/wg-easy/replicated/config.yaml @@ -0,0 +1,4 @@ +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: wg-easy diff --git a/applications/wg-easy/taskfiles/utils.yml b/applications/wg-easy/taskfiles/utils.yml new file mode 100644 index 00000000..7b3a14de --- /dev/null +++ b/applications/wg-easy/taskfiles/utils.yml @@ -0,0 +1,248 @@ +version: "3" + +tasks: + get-kubeconfig: + desc: Get kubeconfig for the test cluster (internal) + internal: true + silent: false + run: once + cmds: + - | + echo "Getting kubeconfig for cluster {{.CLUSTER_NAME}}..." + replicated cluster kubeconfig --name {{.CLUSTER_NAME}} --output-path {{.KUBECONFIG_FILE}} + status: + - test -f {{.KUBECONFIG_FILE}} + + remove-k3s-traefik: + desc: Remove pre-installed Traefik from k3s clusters (internal) + internal: true + silent: false + run: once + status: + - | + # Only check if we need to run this for k3s distributions + if [ "{{.DISTRIBUTION}}" != "k3s" ]; then + exit 0 # Not a k3s cluster, so we're "done" + fi + + # Check if traefik is already removed by looking for the helm releases + KUBECONFIG={{.KUBECONFIG_FILE}} helm list -n kube-system -o json | \ + jq -e 'map(select(.name == "traefik" or .name == "traefik-crd")) | length == 0' >/dev/null + cmds: + - | + # Only run for k3s distributions + if [ "{{.DISTRIBUTION}}" = "k3s" ]; then + echo "Checking for pre-installed Traefik in k3s cluster..." + + # Check if traefik is installed in kube-system namespace + TRAEFIK_CHARTS=$(KUBECONFIG={{.KUBECONFIG_FILE}} helm list -n kube-system -o json | jq -r '.[] | select(.name == "traefik" or .name == "traefik-crd") | .name') + + if [ -n "$TRAEFIK_CHARTS" ]; then + echo "Found pre-installed Traefik charts in kube-system namespace. Removing..." + + for chart in $TRAEFIK_CHARTS; do + echo "Uninstalling chart: $chart" + KUBECONFIG={{.KUBECONFIG_FILE}} helm uninstall $chart -n kube-system --wait + done + + echo "Pre-installed Traefik removed successfully!" + else + echo "No pre-installed Traefik charts found in kube-system namespace." + fi + else + echo "Not a k3s cluster, skipping Traefik removal." + fi + deps: + - get-kubeconfig + + wait-for-cluster: + desc: Wait for cluster to be in running state + internal: true + silent: true + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + TIMEOUT: '{{.TIMEOUT | default "300"}}' + cmds: + - | + echo "Waiting for cluster {{.CLUSTER_NAME}} (timeout {{.TIMEOUT}}s)..." + start=$(date +%s) + attempt=1 + while true; do + CLUSTER_STATUS=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .status') + + if [ "$CLUSTER_STATUS" = "running" ]; then + elapsed=$(($(date +%s) - start)) + echo "Cluster {{.CLUSTER_NAME}} is ready! (took $elapsed seconds)" + break + fi + + elapsed=$(($(date +%s) - start)) + if [ $elapsed -ge {{.TIMEOUT}} ]; then + echo "Timeout after {{.TIMEOUT}} seconds waiting for cluster to be ready" + exit 1 + fi + + printf "\rWaiting... %ds elapsed (attempt %d) - Current status: %s " "$elapsed" "$attempt" "$CLUSTER_STATUS" + sleep 5 + attempt=$((attempt+1)) + done + + + port-operations: + desc: Expose and check status of ports + silent: true + vars: + OPERATION: '{{.OPERATION | default "expose"}}' + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + cmds: + - | + set -e + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}") | .id') + if [ -z "$CLUSTER_ID" ]; then + echo "Error: Could not find cluster with name {{.CLUSTER_NAME}}" + exit 1 + fi + + if [ "{{.OPERATION}}" = "expose" ]; then + echo "Exposing ports for cluster {{.CLUSTER_NAME}} (ID: $CLUSTER_ID)..." + + {{range .EXPOSE_PORTS}} + echo "Exposing port {{.port}} for {{.protocol}}..." + replicated cluster port expose $CLUSTER_ID --port {{.port}} --protocol {{.protocol}} + {{end}} + elif [ "{{.OPERATION}}" = "getenv" ]; then + # Get TF_EXPOSED_URL for HTTPS + TF_EXPOSED_URL=$(replicated cluster port ls $CLUSTER_ID --output json | jq -r '.[] | select(.upstream_port == 30443 and .exposed_ports[0].protocol == "https") | .hostname' | head -n 1) + + # Get TF_EXPOSED_HTTP_URL for HTTP + TF_EXPOSED_HTTP_URL=$(replicated cluster port ls $CLUSTER_ID --output json | jq -r '.[] | select(.upstream_port == 30080 and .exposed_ports[0].protocol == "http") | .hostname' | head -n 1) + + if [ -z "$TF_EXPOSED_URL" ]; then + echo "Error: Could not determine TF_EXPOSED_URL. HTTPS port is not properly exposed." + echo "Please ensure the HTTPS port is exposed before deploying." + exit 1 + fi + + if [ -z "$TF_EXPOSED_HTTP_URL" ]; then + echo "Error: Could not determine TF_EXPOSED_HTTP_URL. HTTP port is not properly exposed." + echo "Please ensure the HTTP port is exposed before deploying." + exit 1 + fi + + echo "TF_EXPOSED_URL=$TF_EXPOSED_URL TF_EXPOSED_HTTP_URL=$TF_EXPOSED_HTTP_URL" + fi + + gcp-operations: + desc: GCP VM operations + internal: true + silent: true + vars: + OPERATION: '{{.OPERATION}}' + GCP_PROJECT: '{{.GCP_PROJECT}}' + GCP_ZONE: '{{.GCP_ZONE}}' + VM_NAME: '{{.VM_NAME}}' + cmds: + - | + if [ -z "{{.GCP_PROJECT}}" ]; then + echo "Error: GCP_PROJECT is required. Please specify with GCP_PROJECT=your-project-id" + exit 1 + fi + + if [ "{{.OPERATION}}" = "create" ]; then + echo "Creating GCP VM instance {{.VM_NAME}}..." + + # Create the VM + gcloud compute instances create {{.VM_NAME}} \ + --project={{.GCP_PROJECT}} \ + --zone={{.GCP_ZONE}} \ + --machine-type={{.GCP_MACHINE_TYPE}} \ + --image-family={{.GCP_IMAGE_FAMILY}} \ + --image-project={{.GCP_IMAGE_PROJECT}} \ + --boot-disk-size={{.GCP_DISK_SIZE}}GB \ + --boot-disk-type={{.GCP_DISK_TYPE}} \ + --labels=expires-on=never,owner={{or (env "GUSER") "user"}} + + # Get the external IP + EXTERNAL_IP=$(gcloud compute instances describe {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} --format='get(networkInterfaces[0].accessConfigs[0].natIP)') + + echo "VM {{.VM_NAME}} created successfully with IP: $EXTERNAL_IP" + echo "You can SSH into the VM with: gcloud compute ssh {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}}" + + # Wait for VM to be fully in RUNNING state (should be immediate, but just in case) + echo "Verifying VM is in RUNNING state..." + start=$(date +%s) + attempt=1 + timeout=30 # 30 seconds timeout + + while true; do + VM_STATUS=$(gcloud compute instances describe {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} --format='get(status)' 2>/dev/null || echo "PENDING") + + if [ "$VM_STATUS" = "RUNNING" ]; then + elapsed=$(($(date +%s) - start)) + echo "VM {{.VM_NAME}} is in RUNNING state! (took $elapsed seconds)" + break + fi + + elapsed=$(($(date +%s) - start)) + if [ $elapsed -ge $timeout ]; then + echo "Timeout after $timeout seconds waiting for VM to be ready" + exit 1 + fi + + printf "\rWaiting... %ds elapsed (attempt %d) - Current status: %s " "$elapsed" "$attempt" "$VM_STATUS" + sleep 1 + attempt=$((attempt+1)) + done + + elif [ "{{.OPERATION}}" = "delete" ]; then + echo "Deleting GCP VM instance {{.VM_NAME}}..." + + # Delete the VM + gcloud compute instances delete {{.VM_NAME}} \ + --project={{.GCP_PROJECT}} \ + --zone={{.GCP_ZONE}} \ + --quiet + + echo "VM {{.VM_NAME}} deleted successfully" + + elif [ "{{.OPERATION}}" = "setup-embedded" ]; then + echo "Setting up embedded cluster on GCP VM {{.VM_NAME}}..." + + # Wait for SSH to be ready with retry logic + echo "Waiting for SSH to be ready on VM {{.VM_NAME}}..." + start=$(date +%s) + attempt=1 + timeout=60 # 60 seconds timeout for SSH + + while true; do + if gcloud compute ssh {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} --command="echo 'SSH is ready'" &>/dev/null; then + elapsed=$(($(date +%s) - start)) + echo "SSH connection established successfully! (took $elapsed seconds)" + break + fi + + elapsed=$(($(date +%s) - start)) + if [ $elapsed -ge $timeout ]; then + echo "Timeout after $timeout seconds waiting for SSH to be ready" + exit 1 + fi + + printf "\rWaiting... %ds elapsed (attempt %d) - SSH not ready yet " "$elapsed" "$attempt" + sleep 2 + attempt=$((attempt+1)) + done + + # Run installation commands on the VM + echo "Installing embedded cluster on VM..." + gcloud compute ssh {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}} --command=" + set -e + echo 'Downloading {{.APP_NAME}} installer...' + curl -f 'https://replicated.app/embedded/{{.APP_NAME}}/{{.CHANNEL}}' -H 'Authorization: {{.AUTH_TOKEN}}' -o {{.APP_NAME}}-{{.CHANNEL}}.tgz + + echo 'Extracting installer...' + tar -xvzf {{.APP_NAME}}-{{.CHANNEL}}.tgz + " + + echo "Embedded cluster setup initiated on VM {{.VM_NAME}}" + echo "You can SSH into the VM with: gcloud compute ssh {{.VM_NAME}} --project={{.GCP_PROJECT}} --zone={{.GCP_ZONE}}" + fi diff --git a/applications/wg-easy/traefik/Chart.yaml b/applications/wg-easy/traefik/Chart.yaml new file mode 100644 index 00000000..c73b0b63 --- /dev/null +++ b/applications/wg-easy/traefik/Chart.yaml @@ -0,0 +1,11 @@ +name: traefik +version: 1.0.0 +appVersion: latest +apiVersion: v2 +dependencies: + - name: traefik + version: '28.0.0' + repository: https://traefik.github.io/charts + - name: templates + version: '*' + repository: file://../charts/templates diff --git a/applications/wg-easy/traefik/replicated/helmChart-traefik.yaml b/applications/wg-easy/traefik/replicated/helmChart-traefik.yaml new file mode 100644 index 00000000..74d08de8 --- /dev/null +++ b/applications/wg-easy/traefik/replicated/helmChart-traefik.yaml @@ -0,0 +1,12 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: traefik +spec: + chart: + name: traefik + weight: 2 + helmUpgradeFlags: + - --wait + namespace: traefik + builder: {} diff --git a/applications/wg-easy/traefik/templates/certificate.yaml b/applications/wg-easy/traefik/templates/certificate.yaml new file mode 100644 index 00000000..b4798e7b --- /dev/null +++ b/applications/wg-easy/traefik/templates/certificate.yaml @@ -0,0 +1,64 @@ +{{ if .Values.certs.selfSigned }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: traefik-self-signed +spec: + commonName: traefik + secretName: traefik-self-signed + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + group: cert-manager.io +{{- end }} +{{- if .Values.certs.installProduction }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: traefik-cert +spec: + secretName: traefik-cert + duration: 2160h0m0s + renewBefore: 720h0m0s + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + - client auth + dnsNames: + {{- range .Values.certs.dnsNames }} + - {{ . | quote }} + {{- end }} + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer +{{- end }} +{{- if .Values.certs.installStaging }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: traefik-cert-staging +spec: + secretName: traefik-cert-staging + duration: 2160h0m0s + renewBefore: 720h0m0s + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + - client auth + dnsNames: + {{- range .Values.certs.dnsNames }} + - {{ . | quote }} + {{- end }} + issuerRef: + name: letsencrypt-staging + kind: ClusterIssuer +{{- end }} diff --git a/applications/wg-easy/traefik/templates/tls-options.yaml b/applications/wg-easy/traefik/templates/tls-options.yaml new file mode 100644 index 00000000..a34cff6e --- /dev/null +++ b/applications/wg-easy/traefik/templates/tls-options.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.io/v1alpha1 +kind: TLSOption +metadata: + name: default + namespace: traefik +spec: + minVersion: VersionTLS12 + #maxVersion: VersionTLS13 diff --git a/applications/wg-easy/traefik/values.yaml b/applications/wg-easy/traefik/values.yaml new file mode 100644 index 00000000..e2567d7f --- /dev/null +++ b/applications/wg-easy/traefik/values.yaml @@ -0,0 +1,21 @@ +certs: + selfSigned: true + installProduction: false + installStaging: false + dnsNames: [] +traefik: + service: + type: NodePort + ports: + web: + nodePort: 80 + redirectTo: + port: websecure + websecure: + nodePort: 443 + tlsStore: + default: + certificates: + - secretName: traefik-self-signed + defaultCertificate: + secretName: traefik-self-signed diff --git a/applications/wg-easy/wg-easy/Chart.yaml b/applications/wg-easy/wg-easy/Chart.yaml new file mode 100644 index 00000000..5a78dd37 --- /dev/null +++ b/applications/wg-easy/wg-easy/Chart.yaml @@ -0,0 +1,10 @@ +name: wg-easy +version: 1.0.0 +apiVersion: v2 +dependencies: + - name: wg-easy + version: '1.0.0' + repository: https://chris-sanders.github.io/helm-charts + - name: templates + version: '*' + repository: file://../charts/templates diff --git a/applications/wg-easy/wg-easy/replicated/config.yaml b/applications/wg-easy/wg-easy/replicated/config.yaml new file mode 100644 index 00000000..2290ec1c --- /dev/null +++ b/applications/wg-easy/wg-easy/replicated/config.yaml @@ -0,0 +1,37 @@ +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: not-used +spec: + groups: + - name: wireguard-config + title: Wireguard + description: Wireguard configuration + items: + #- name: accept + # title: Systemd has been updated + # help_text: | + # Add `--profile=ip-forward` to k0scontroller.service + # Run `systemd daemon-reload` + # Restart `k0scontroller.service` + # type: bool + # required: true + - name: password + title: Admin password + type: password + required: true + - name: domain + title: IP or domain + help_text: Domain or IP which the vpn is accessible on + type: text + required: true + #- name: web-port + # title: Web port + # help_text: This is the port the admin UI will be served on + # type: text + # default: "51821" + - name: vpn-port + title: vpn port + help_text: This port must be accessible remotely + type: text + default: "20000" diff --git a/applications/wg-easy/wg-easy/replicated/helmChart-wg-easy.yaml b/applications/wg-easy/wg-easy/replicated/helmChart-wg-easy.yaml new file mode 100644 index 00000000..9f5199c2 --- /dev/null +++ b/applications/wg-easy/wg-easy/replicated/helmChart-wg-easy.yaml @@ -0,0 +1,36 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: wg-easy +spec: + chart: + name: wg-easy + weight: 3 + + # helmUpgradeFlags specifies additional flags to pass to the `helm upgrade` command. + helmUpgradeFlags: + - --skip-crds + - --timeout + - 30s + - --history-max=15 + - --wait + + values: + wg-easy: + services: + vpn: + # TODO: Template for stand alone KOTS install? + #type: NodePort + ports: + udp: + nodePort: repl{{ ConfigOption `vpn-port` | ParseInt }} + wireguard: + password: repl{{ ConfigOption `password` }} + host: repl{{ ConfigOption `domain` }} + port: repl{{ ConfigOption `vpn-port` | ParseInt }} + templates: + traefikRoutes: + web-tls: + hostName: repl{{ ConfigOption `domain` }} + namespace: wg-easy + builder: {} diff --git a/applications/wg-easy/wg-easy/values.yaml b/applications/wg-easy/wg-easy/values.yaml new file mode 100644 index 00000000..e033dfaa --- /dev/null +++ b/applications/wg-easy/wg-easy/values.yaml @@ -0,0 +1,40 @@ +wg-easy: + global: + fullNameOverride: public + apps: + wg-easy: + fullNameOverride: public + containers: + wg-container: + resources: + requests: + cpu: 5m + memory: 35Mi + persistence: + config: + persistentVolumeClaim: + spec: + resources: + requests: + storage: 1Gi + services: + vpn: + type: NodePort + wireguard: + password: "testpass" + host: "example.com" + port: 51820 # This is used in the postUp + defaultAddress: "10.10.10.x" + defaultDns: "1.1.1.1" + allowedIps: "0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 224.0.0.0/3" + postUp: "iptables -A FORWARD -i wg0 -o eth0 -d 192.168.0.0/16,172.16.0.0/12,10.0.0.0/8 -j DROP; iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -j MASQUERADE; iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT" +templates: + traefikRoutes: + web-tls: + hostName: "example.com" + serviceName: public-web + servicePort: 51821 + web: + hostName: "example.com" + serviceName: public-web + servicePort: 51821