diff --git a/.github/actions/chart-package/action.yml b/.github/actions/chart-package/action.yml new file mode 100644 index 00000000..9b35478a --- /dev/null +++ b/.github/actions/chart-package/action.yml @@ -0,0 +1,51 @@ +name: 'Package Helm Charts' +description: 'Package all Helm charts and prepare release artifacts' +inputs: + app-dir: + description: 'Application directory containing charts' + default: 'applications/wg-easy' + helm-version: + description: 'Helm version to use' + default: '3.17.3' + use-cache: + description: 'Whether to use dependency cache' + default: 'true' +outputs: + release-path: + description: 'Path to release artifacts' + value: ${{ inputs.app-dir }}/release + +runs: + using: 'composite' + steps: + - name: Setup tools + uses: ./.github/actions/setup-tools + with: + helm-version: ${{ inputs.helm-version }} + + - name: Cache Helm dependencies + if: inputs.use-cache == 'true' + uses: actions/cache@v4 + with: + path: | + ${{ inputs.app-dir }}/charts/*/charts + ${{ inputs.app-dir }}/Chart.lock + key: helm-deps-${{ hashFiles(format('{0}/charts/*/Chart.yaml', inputs.app-dir)) }} + + - name: Package charts + shell: bash + working-directory: ${{ inputs.app-dir }} + run: task chart-package-all + + - name: Verify release contents + shell: bash + working-directory: ${{ inputs.app-dir }} + run: | + echo "Verifying release directory contents:" + ls -la release/ + echo "Checking required files:" + test -f release/application.yaml + test -f release/config.yaml + test -f release/cluster.yaml + echo "Chart packages:" + find release/ -name "*.tgz" | wc -l | grep -v "^0$" \ No newline at end of file diff --git a/.github/actions/chart-validate/action.yml b/.github/actions/chart-validate/action.yml new file mode 100644 index 00000000..6df6dc18 --- /dev/null +++ b/.github/actions/chart-validate/action.yml @@ -0,0 +1,35 @@ +name: 'Validate Helm Charts' +description: 'Validate all Helm charts using Task-based operations' +inputs: + app-dir: + description: 'Application directory containing charts' + default: 'applications/wg-easy' + helm-version: + description: 'Helm version to use' + default: '3.17.3' + use-cache: + description: 'Whether to use dependency cache' + default: 'true' + +runs: + using: 'composite' + steps: + - name: Setup tools + uses: ./.github/actions/setup-tools + with: + helm-version: ${{ inputs.helm-version }} + install-helmfile: 'true' + + - name: Cache Helm dependencies + if: inputs.use-cache == 'true' + uses: actions/cache@v4 + with: + path: | + ${{ inputs.app-dir }}/charts/*/charts + ${{ inputs.app-dir }}/Chart.lock + key: helm-deps-${{ hashFiles(format('{0}/charts/*/Chart.yaml', inputs.app-dir)) }} + + - name: Validate charts + shell: bash + working-directory: ${{ inputs.app-dir }} + run: task chart-validate \ No newline at end of file diff --git a/.github/actions/replicated-release/action.yml b/.github/actions/replicated-release/action.yml new file mode 100644 index 00000000..724e0cff --- /dev/null +++ b/.github/actions/replicated-release/action.yml @@ -0,0 +1,48 @@ +name: 'Create Replicated Release' +description: 'Create channel and release using Task-based operations' +inputs: + app-dir: + description: 'Application directory containing charts' + default: 'applications/wg-easy' + channel-name: + description: 'Release channel name' + required: true + channel-id: + description: 'Release channel ID (optional, takes precedence over channel-name)' + required: false + release-version: + description: 'Release version' + default: '0.0.1' + release-notes: + description: 'Release notes' + default: 'Release created via GitHub Actions' + +outputs: + channel-id: + description: 'Channel ID created or found' + value: ${{ steps.channel.outputs.channel-id }} + +runs: + using: 'composite' + steps: + - name: Setup tools + uses: ./.github/actions/setup-tools + + - name: Create channel + id: channel + shell: bash + working-directory: ${{ inputs.app-dir }} + run: | + CHANNEL_ID=$(task channel-create RELEASE_CHANNEL="${{ inputs.channel-name }}" --silent | tail -1) + echo "channel-id=$CHANNEL_ID" >> $GITHUB_OUTPUT + echo "Created/found channel with ID: $CHANNEL_ID" + + - name: Create release + shell: bash + working-directory: ${{ inputs.app-dir }} + run: | + task release-create \ + RELEASE_CHANNEL_ID="${{ steps.channel.outputs.channel-id }}" \ + RELEASE_CHANNEL="${{ inputs.channel-name }}" \ + RELEASE_VERSION="${{ inputs.release-version }}" \ + RELEASE_NOTES="${{ inputs.release-notes }}" \ No newline at end of file diff --git a/.github/actions/setup-tools/action.yml b/.github/actions/setup-tools/action.yml new file mode 100644 index 00000000..66032071 --- /dev/null +++ b/.github/actions/setup-tools/action.yml @@ -0,0 +1,94 @@ +name: 'Setup Common Tools' +description: 'Setup Helm, Task, yq, kubectl, preflight, helmfile, and Replicated CLI' +inputs: + helm-version: + description: 'Helm version' + default: '3.17.3' + kubectl-version: + description: 'kubectl version' + default: 'v1.30.0' + app-dir: + description: 'Application directory' + default: 'applications/wg-easy' + install-kubectl: + description: 'Whether to install kubectl' + default: 'false' + install-preflight: + description: 'Whether to install preflight' + default: 'false' + install-helmfile: + description: 'Whether to install helmfile' + default: 'false' + +runs: + using: 'composite' + steps: + - name: Setup Helm + uses: azure/setup-helm@v4 + with: + version: ${{ inputs.helm-version }} + + - name: Setup Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ github.token }} + + - name: Setup kubectl + if: inputs.install-kubectl == 'true' + uses: azure/setup-kubectl@v4 + with: + version: ${{ inputs.kubectl-version }} + + - name: Cache tools + uses: actions/cache@v4 + with: + path: | + /usr/local/bin/yq + /usr/local/bin/preflight + /usr/local/bin/helmfile + ~/.replicated + key: tools-${{ runner.os }}-yq-v4.44.3-preflight-v0.95.0-helmfile-v0.170.0-replicated-latest + restore-keys: | + tools-${{ runner.os }}-yq-v4.44.3-preflight-v0.95.0-helmfile-v0.170.0- + + - name: Install yq + shell: bash + run: | + if [ ! -f /usr/local/bin/yq ]; then + echo "Installing yq v4.44.3..." + sudo wget https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 -O /usr/local/bin/yq + sudo chmod +x /usr/local/bin/yq + else + echo "yq already installed (cached)" + fi + + - name: Install preflight CLI + if: inputs.install-preflight == 'true' + shell: bash + run: | + if [ ! -f /usr/local/bin/preflight ]; then + echo "Installing preflight v0.95.0..." + curl -L https://github.com/replicatedhq/troubleshoot/releases/download/v0.95.0/preflight_linux_amd64.tar.gz | tar xz + sudo mv preflight /usr/local/bin/ + else + echo "preflight already installed (cached)" + fi + + - name: Install helmfile + if: inputs.install-helmfile == 'true' + shell: bash + run: | + if [ ! -f /usr/local/bin/helmfile ]; then + echo "Installing helmfile v0.170.0..." + curl -L https://github.com/helmfile/helmfile/releases/download/v0.170.0/helmfile_0.170.0_linux_amd64.tar.gz | tar xz + sudo mv helmfile /usr/local/bin/ + sudo chmod +x /usr/local/bin/helmfile + else + echo "helmfile already installed (cached)" + fi + + - name: Install Replicated CLI + shell: bash + working-directory: ${{ inputs.app-dir }} + run: task utils:install-replicated-cli \ No newline at end of file diff --git a/.github/actions/test-deployment/action.yml b/.github/actions/test-deployment/action.yml new file mode 100644 index 00000000..b193b7b9 --- /dev/null +++ b/.github/actions/test-deployment/action.yml @@ -0,0 +1,125 @@ +name: 'Test Deployment' +description: 'Test deployment using customer workflow' +inputs: + app-dir: + description: 'Application directory containing charts' + default: 'applications/wg-easy' + customer-name: + description: 'Customer name for testing' + required: true + cluster-name: + description: 'Cluster name for testing' + required: true + channel-name: + description: 'Channel name for testing' + required: false + channel-id: + description: 'Channel ID for testing (optional, takes precedence over channel-name)' + required: false + helm-version: + description: 'Helm version to use' + default: '3.17.3' + cleanup: + description: 'Whether to cleanup resources after testing' + default: 'false' + +outputs: + customer-license: + description: 'Customer license ID used for testing' + value: ${{ steps.license.outputs.license-id }} + +runs: + using: 'composite' + steps: + - name: Setup tools + uses: ./.github/actions/setup-tools + with: + helm-version: ${{ inputs.helm-version }} + install-helmfile: 'true' + + - name: Create customer + shell: bash + working-directory: ${{ inputs.app-dir }} + env: + REPLICATED_APP: ${{ env.REPLICATED_APP }} + REPLICATED_API_TOKEN: ${{ env.REPLICATED_API_TOKEN }} + run: | + if [ -n "${{ inputs.channel-id }}" ]; then + task customer-create \ + CUSTOMER_NAME="${{ inputs.customer-name }}" \ + RELEASE_CHANNEL_ID="${{ inputs.channel-id }}" + else + task customer-create \ + CUSTOMER_NAME="${{ inputs.customer-name }}" \ + RELEASE_CHANNEL="${{ inputs.channel-name }}" + fi + + - name: Get customer license + id: license + shell: bash + working-directory: ${{ inputs.app-dir }} + env: + REPLICATED_APP: ${{ env.REPLICATED_APP }} + REPLICATED_API_TOKEN: ${{ env.REPLICATED_API_TOKEN }} + run: | + LICENSE_ID=$(task utils:get-customer-license CUSTOMER_NAME="${{ inputs.customer-name }}" --silent | tail -1) + echo "license-id=$LICENSE_ID" >> $GITHUB_OUTPUT + echo "::add-mask::$LICENSE_ID" + + - name: Create cluster with retry + uses: nick-fields/retry@v3.0.2 + with: + timeout_minutes: 20 + retry_wait_seconds: 30 + max_attempts: 3 + command: | + cd ${{ inputs.app-dir }} + export REPLICATED_APP="${{ env.REPLICATED_APP }}" + export REPLICATED_API_TOKEN="${{ env.REPLICATED_API_TOKEN }}" + task cluster-create CLUSTER_NAME="${{ inputs.cluster-name }}" + + - name: Setup cluster + shell: bash + working-directory: ${{ inputs.app-dir }} + env: + REPLICATED_APP: ${{ env.REPLICATED_APP }} + REPLICATED_API_TOKEN: ${{ env.REPLICATED_API_TOKEN }} + run: | + task setup-kubeconfig CLUSTER_NAME="${{ inputs.cluster-name }}" + task cluster-ports-expose CLUSTER_NAME="${{ inputs.cluster-name }}" + + - name: Deploy application + shell: bash + working-directory: ${{ inputs.app-dir }} + env: + REPLICATED_APP: ${{ env.REPLICATED_APP }} + REPLICATED_API_TOKEN: ${{ env.REPLICATED_API_TOKEN }} + run: | + if [ -n "${{ inputs.channel-id }}" ]; then + task customer-helm-install \ + CUSTOMER_NAME="${{ inputs.customer-name }}" \ + CLUSTER_NAME="${{ inputs.cluster-name }}" \ + CHANNEL_ID="${{ inputs.channel-id }}" \ + REPLICATED_LICENSE_ID="${{ steps.license.outputs.license-id }}" + else + task customer-helm-install \ + CUSTOMER_NAME="${{ inputs.customer-name }}" \ + CLUSTER_NAME="${{ inputs.cluster-name }}" \ + CHANNEL_SLUG="${{ inputs.channel-name }}" \ + REPLICATED_LICENSE_ID="${{ steps.license.outputs.license-id }}" + fi + + - name: Run tests + shell: bash + working-directory: ${{ inputs.app-dir }} + env: + REPLICATED_APP: ${{ env.REPLICATED_APP }} + REPLICATED_API_TOKEN: ${{ env.REPLICATED_API_TOKEN }} + run: task test + + # - name: Cleanup resources + # if: inputs.cleanup == 'true' + # shell: bash + # working-directory: ${{ inputs.app-dir }} + # run: | + # task cleanup-pr-resources BRANCH_NAME="${{ inputs.customer-name }}" diff --git a/.github/workflows/wg-easy-image.yml b/.github/workflows/wg-easy-image.yml index c22ca3b3..a3c41da6 100644 --- a/.github/workflows/wg-easy-image.yml +++ b/.github/workflows/wg-easy-image.yml @@ -3,6 +3,7 @@ name: WG-Easy Image CI on: push: branches: [ main ] + tags: [ 'v*' ] paths: - 'applications/wg-easy/**' - '.github/workflows/wg-easy-image.yml' @@ -13,16 +14,27 @@ on: workflow_dispatch: env: - DEV_CONTAINER_REGISTRY: ghcr.io - DEV_CONTAINER_IMAGE: replicatedhq/platform-examples/wg-easy-tools + # GitHub Container Registry + GHCR_REGISTRY: ghcr.io + GHCR_IMAGE: replicatedhq/platform-examples/wg-easy-tools + # Google Artifact Registry + GAR_LOCATION: us-central1 + GAR_PROJECT_ID: replicated-qa + GAR_REPOSITORY: wg-easy + GAR_IMAGE: wg-easy-tools + # Replicated Registry + REPLICATED_REGISTRY: registry.replicated.com + REPLICATED_APP: wg-easy-cre + REPLICATED_IMAGE: wg-easy-tools jobs: - build-and-push: + build: runs-on: ubuntu-latest - permissions: - contents: read - packages: write - + outputs: + image-digest: ${{ steps.build.outputs.digest }} + metadata: ${{ steps.meta.outputs.json }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -33,32 +45,189 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ${{ env.DEV_CONTAINER_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Set branch variables + id: vars + run: | + # Check if this is a tag push + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG_NAME="${{ github.ref_name }}" + echo "is-tag=true" >> $GITHUB_OUTPUT + echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "Tag: $TAG_NAME" + else + # Get branch name and normalize to lowercase with hyphens + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + NORMALIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | tr '/' '-') + IS_MAIN=${{ github.ref_name == 'main' || github.ref_name == 'refs/heads/main' }} + echo "is-tag=false" >> $GITHUB_OUTPUT + echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "normalized-branch=$NORMALIZED_BRANCH" >> $GITHUB_OUTPUT + echo "is-main=$IS_MAIN" >> $GITHUB_OUTPUT + echo "Branch: $BRANCH_NAME, Normalized: $NORMALIZED_BRANCH, Is Main: $IS_MAIN" + fi - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.DEV_CONTAINER_REGISTRY }}/${{ env.DEV_CONTAINER_IMAGE }} + images: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE }} + ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.GAR_PROJECT_ID }}/${{ env.GAR_REPOSITORY }}/${{ env.GAR_IMAGE }} + ${{ env.REPLICATED_REGISTRY }}/${{ env.REPLICATED_APP }}/${{ env.REPLICATED_IMAGE }} tags: | + # Git tag releases (semver tags) + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # Main branch tags type=raw,value=latest,enable={{is_default_branch}} - type=sha,format=short - type=ref,event=branch - type=ref,event=pr + type=raw,value=sha-{{sha}},enable={{is_default_branch}} + # Non-main branch tags - branch name as "latest" for that branch + type=raw,value=${{ steps.vars.outputs.normalized-branch }},enable=${{ steps.vars.outputs.is-tag == 'false' && steps.vars.outputs.is-main == 'false' }} + # SHA-suffixed tags for all branches (main and non-main) + type=raw,value=${{ steps.vars.outputs.normalized-branch }}-sha-{{sha}},enable=${{ steps.vars.outputs.is-tag == 'false' && steps.vars.outputs.is-main == 'false' }} - - name: Build and push image + - name: Build multi-arch image + id: build uses: docker/build-push-action@v6 with: context: applications/wg-easy file: applications/wg-easy/container/Containerfile platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: false tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + push-ghcr: + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract GHCR tags + id: ghcr-tags + run: | + GHCR_TAGS=$(echo '${{ needs.build.outputs.metadata }}' | jq -r '.tags[]' | grep "^${{ env.GHCR_REGISTRY }}" | tr '\n' ',') + echo "tags=${GHCR_TAGS%,}" >> $GITHUB_OUTPUT + echo "GHCR tags: ${GHCR_TAGS%,}" + + - name: Build and push to GHCR + uses: docker/build-push-action@v6 + with: + context: applications/wg-easy + file: applications/wg-easy/container/Containerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.ghcr-tags.outputs.tags }} + labels: ${{ needs.build.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + push-gar: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker ${{ env.GAR_LOCATION }}-docker.pkg.dev + + - name: Log in to Google Artifact Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev + username: _json_key + password: ${{ secrets.GCP_SA_KEY }} + + - name: Extract GAR tags + id: gar-tags + run: | + GAR_TAGS=$(echo '${{ needs.build.outputs.metadata }}' | jq -r '.tags[]' | grep "^${{ env.GAR_LOCATION }}-docker.pkg.dev" | tr '\n' ',') + echo "tags=${GAR_TAGS%,}" >> $GITHUB_OUTPUT + echo "GAR tags: ${GAR_TAGS%,}" + + - name: Build and push to GAR + uses: docker/build-push-action@v6 + with: + context: applications/wg-easy + file: applications/wg-easy/container/Containerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.gar-tags.outputs.tags }} + labels: ${{ needs.build.outputs.labels }} + cache-from: type=gha + + push-replicated: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup tools + uses: ./.github/actions/setup-tools + with: + app-dir: applications/wg-easy + + - name: Log in to Replicated Registry + run: | + replicated registry login + docker login ${{ env.REPLICATED_REGISTRY }} -u "${{ secrets.WG_EASY_REPLICATED_API_TOKEN }}" -p "${{ secrets.WG_EASY_REPLICATED_API_TOKEN }}" + env: + REPLICATED_API_TOKEN: ${{ secrets.WG_EASY_REPLICATED_API_TOKEN }} + + - name: Extract Replicated tags + id: replicated-tags + run: | + REPLICATED_TAGS=$(echo '${{ needs.build.outputs.metadata }}' | jq -r '.tags[]' | grep "^${{ env.REPLICATED_REGISTRY }}" | tr '\n' ',') + echo "tags=${REPLICATED_TAGS%,}" >> $GITHUB_OUTPUT + echo "Replicated tags: ${REPLICATED_TAGS%,}" + + - name: Build and push to Replicated Registry + uses: docker/build-push-action@v6 + with: + context: applications/wg-easy + file: applications/wg-easy/container/Containerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.replicated-tags.outputs.tags }} + labels: ${{ needs.build.outputs.labels }} + cache-from: type=gha \ No newline at end of file diff --git a/.github/workflows/wg-easy-pr-validation.yaml b/.github/workflows/wg-easy-pr-validation.yaml new file mode 100644 index 00000000..ec76e2a6 --- /dev/null +++ b/.github/workflows/wg-easy-pr-validation.yaml @@ -0,0 +1,152 @@ +--- +name: WG-Easy PR Validation - build, release, install + +on: + pull_request: + branches: [main] + paths: + - 'applications/wg-easy/**' + - '.github/workflows/wg-easy-pr-validation.yaml' + workflow_dispatch: + inputs: + test_mode: + description: 'Run in test mode' + required: false + default: 'true' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + APP_DIR: applications/wg-easy + REPLICATED_API_TOKEN: ${{ secrets.WG_EASY_REPLICATED_API_TOKEN }} + REPLICATED_APP: ${{ secrets.WG_EASY_REPLICATED_APP }} + HELM_VERSION: "3.17.3" + KUBECTL_VERSION: "v1.30.0" + +jobs: + setup: + runs-on: ubuntu-22.04 + outputs: + branch-name: ${{ steps.vars.outputs.branch-name }} + channel-name: ${{ steps.vars.outputs.channel-name }} + steps: + - name: Set branch and channel variables + id: vars + run: | + # Branch name preserves original case for resource naming (clusters, customers) + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + # Channel name is normalized to lowercase with hyphens for Replicated channels + CHANNEL_NAME=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | tr '/' '-') + echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "channel-name=$CHANNEL_NAME" >> $GITHUB_OUTPUT + echo "Branch: $BRANCH_NAME, Channel: $CHANNEL_NAME" + + validate-charts: + runs-on: ubuntu-22.04 + needs: setup + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate charts + uses: ./.github/actions/chart-validate + with: + app-dir: ${{ env.APP_DIR }} + helm-version: ${{ env.HELM_VERSION }} + + - name: Validate Taskfile syntax + run: task --list-all + working-directory: ${{ env.APP_DIR }} + + build-and-package: + runs-on: ubuntu-22.04 + needs: [setup, validate-charts] + outputs: + release-path: ${{ steps.package.outputs.release-path }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Package charts + id: package + uses: ./.github/actions/chart-package + with: + app-dir: ${{ env.APP_DIR }} + helm-version: ${{ env.HELM_VERSION }} + + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: wg-easy-release-${{ github.run_number }} + path: ${{ steps.package.outputs.release-path }} + retention-days: 7 + + create-release: + runs-on: ubuntu-22.04 + needs: [setup, build-and-package] + outputs: + channel-id: ${{ steps.release.outputs.channel-id }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + name: wg-easy-release-${{ github.run_number }} + path: ${{ env.APP_DIR }}/release + + - name: Create Replicated release + id: release + uses: ./.github/actions/replicated-release + with: + app-dir: ${{ env.APP_DIR }} + channel-name: ${{ needs.setup.outputs.channel-name }} + release-notes: "PR validation release for ${{ needs.setup.outputs.branch-name }}" + + test-deployment: + runs-on: ubuntu-22.04 + needs: [setup, create-release] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Test deployment + uses: ./.github/actions/test-deployment + with: + app-dir: ${{ env.APP_DIR }} + customer-name: ${{ needs.setup.outputs.channel-name }} + cluster-name: ${{ needs.setup.outputs.channel-name }} + channel-id: ${{ needs.create-release.outputs.channel-id }} + helm-version: ${{ env.HELM_VERSION }} + cleanup: 'false' + + - name: Upload debug logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: debug-logs-${{ github.run_number }} + path: | + /tmp/*.log + ~/.replicated/ + + cleanup: + runs-on: ubuntu-22.04 + needs: [setup, test-deployment] + if: always() + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup tools + uses: ./.github/actions/setup-tools + with: + app-dir: ${{ env.APP_DIR }} + + - name: Cleanup PR resources + run: | + task cleanup-pr-resources BRANCH_NAME="${{ needs.setup.outputs.channel-name }}" || echo "Cleanup completed with some warnings" + working-directory: ${{ env.APP_DIR }} + diff --git a/.gitignore b/.gitignore index 08372041..37d3b44f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,8 +43,6 @@ __pycache__/ # Cursor .cursor/ -# Claude -.claude/ # Mlflow specific applications/mlflow/tests/.venv/ diff --git a/.yamllint b/.yamllint new file mode 100644 index 00000000..601d7031 --- /dev/null +++ b/.yamllint @@ -0,0 +1,10 @@ +extends: default + +rules: + line-length: + max: 120 + level: warning + truthy: + allowed-values: ['true', 'false', 'on', 'off', 'yes', 'no'] + comments: + min-spaces-from-content: 1 \ No newline at end of file diff --git a/applications/wg-easy/.claude/settings.json b/applications/wg-easy/.claude/settings.json new file mode 100644 index 00000000..e2878ec3 --- /dev/null +++ b/applications/wg-easy/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "permissions": { + "allow": [ + "Bash(task --list)", + "Bash(task cluster-create)", + "Bash(task cluster-delete)", + "Bash(task cluster-list)", + "Bash(task cluster-ports-expose)", + "Bash(task customer-create:*)", + "Bash(task customer-ls)", + "Bash(task customer-delete:*)", + "Bash(task dependencies-update)", + "Bash(task dev:start)", + "Bash(task dev:shell)", + "Bash(task dev:stop)", + "Bash(task dev:build-image)", + "Bash(task full-test-cycle)", + "Bash(task helm-install)", + "Bash(task release-create:*)", + "Bash(task release-prepare)", + "Bash(task setup-kubeconfig)", + "Bash(task test)", + "Bash(helm lint:*)", + "Bash(helmfile template:*)", + "Bash(kubectl:*)", + "Bash(KUBECONFIG=./test-cluster.kubeconfig kubectl:*)" + ], + "deny": [] + }, + "timeout": { + "Bash(task helm-install)": 1200000, + "Bash(task full-test-cycle)": 1800000, + "Bash(task cluster-create)": 600000 + } +} \ No newline at end of file diff --git a/applications/wg-easy/CLAUDE.md b/applications/wg-easy/CLAUDE.md index a7211b3e..0d337571 100644 --- a/applications/wg-easy/CLAUDE.md +++ b/applications/wg-easy/CLAUDE.md @@ -2,6 +2,36 @@ This file contains common commands and workflows for working with the WG-Easy Helm chart project. +## Current Project Status + +**Branch:** `adamancini/gh-actions` +**Last Updated:** December 27, 2024 + +### Recent Changes +- Enhanced customer workflow with full test cycle and improved task documentation +- Updated Helm chart dependencies and fixed imagePullSecret template +- Added customer-helm-install task for deployment using replicated environment +- Implemented automatic name normalization for git branch names in cluster, customer, and channel creation +- Added comprehensive timeout and monitoring guidance for Helm operations +- Enhanced background monitoring capabilities for detecting early deployment failures + +### Key Features +- **Automatic Name Normalization**: Git branch names are automatically normalized (replacing `/`, `_`, `.` with `-`) to match Replicated Vendor Portal backend slug format +- **Enhanced Customer Workflow**: Complete customer lifecycle management from creation to deployment +- **Improved Error Detection**: Background monitoring and early timeout detection for ImagePullBackOff scenarios +- **Multi-Registry Support**: Container images published to GHCR, Google Artifact Registry, and Replicated Registry +- **Comprehensive Testing**: Full test cycles with cluster creation, deployment, and cleanup automation + +### Recent Improvements +- Enhanced Taskfile.yaml with automatic name normalization for cluster, customer, and channel operations +- Improved utils.yml with normalized customer name handling in license retrieval +- Updated documentation with comprehensive guidance for background monitoring and timeout detection +- Streamlined customer workflow commands to use git branch names directly +- **Optimized GitHub Actions workflows** with Task-based operations and reusable actions +- **Added chart validation tasks** for consistent linting and templating across environments +- **Implemented PR validation cycle** with automated cleanup and better error handling +- **Enhanced channel management** with unique channel ID support to avoid ambiguous channel names + ## Core Principles The WG-Easy Helm Chart pattern is built on five fundamental principles: @@ -68,12 +98,19 @@ Use tools to automate repetitive tasks, reducing human error and increasing deve ## Architecture Overview 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 +### Taskfile Development Guidelines + +When developing or modifying tasks in the Taskfile: + +⚠️ **Important**: Always update the [task dependency graph](task-dependency-graph.md) when adding, removing, or changing task dependencies. The graph provides critical visibility into task relationships and workflow dependencies for both development and CI/CD operations. + ## `wg-easy` Chart wg-easy uses the `bjw-s/common` [library chart](https://github.com/bjw-s-labs/helm-charts/tree/main) to generate Kubernetes resources. Library charts are commonly used to create DRY templates when authoring Helm charts. @@ -125,14 +162,35 @@ task cluster-delete # Update Helm dependencies for all charts task dependencies-update +# Chart validation and linting +task chart-lint-all # Lint all charts +task chart-template-all # Template all charts for syntax validation +task chart-validate # Complete validation (lint + template + helmfile) +task chart-package-all # Package all charts for distribution + # Install all charts using Helmfile task helm-install +# Install charts for a specific customer (requires pre-setup) +# By default, use current git branch name for customer, cluster, and channel names +# Note: names are automatically normalized (/, _, . replaced with -) by the tasks +# Use CHANNEL_ID for precise channel targeting or CHANNEL_SLUG for channel name +task customer-helm-install CUSTOMER_NAME=$(git branch --show-current) CLUSTER_NAME=$(git branch --show-current) REPLICATED_LICENSE_ID=xxx CHANNEL_ID=your-channel-id + # Run tests task test # Full test cycle (create cluster, deploy, test, delete) task full-test-cycle + +# Complete customer workflow (create cluster, customer, deploy, test, no cleanup) +# By default, use current git branch name for customer and cluster names +# Note: names are automatically normalized (/, _, . replaced with -) by the tasks +task customer-full-test-cycle CUSTOMER_NAME=$(git branch --show-current) CLUSTER_NAME=$(git branch --show-current) + +# PR validation and cleanup +task pr-validation-cycle BRANCH_NAME=$(git branch --show-current) # Complete PR validation workflow +task cleanup-pr-resources BRANCH_NAME=$(git branch --show-current) # Cleanup PR-related resources ``` ## Release Management @@ -144,12 +202,43 @@ task release-prepare # Create and promote a release task release-create RELEASE_VERSION=x.y.z RELEASE_CHANNEL=Unstable +# Channel management (returns channel ID for unique identification) +task channel-create RELEASE_CHANNEL=channel-name +task channel-delete RELEASE_CHANNEL_ID=channel-id + # Customer management -task customer-create CUSTOMER_NAME=example +# By default, use current git branch name for customer name +# Note: names are automatically normalized (/, _, . replaced with -) by the tasks +# Use RELEASE_CHANNEL_ID for precise channel targeting or RELEASE_CHANNEL for channel name +task customer-create CUSTOMER_NAME=$(git branch --show-current) RELEASE_CHANNEL_ID=your-channel-id task customer-ls task customer-delete CUSTOMER_ID=your-customer-id ``` +## Name Normalization + +The WG-Easy workflow automatically normalizes customer, cluster, and channel names by replacing common git branch delimiters (`/`, `_`, `.`) with hyphens (`-`). This normalization serves two important purposes: + +1. **Vendor Portal Backend Compatibility**: Cluster and channel slugs in the Replicated Vendor Portal backend use hyphenated naming conventions +2. **Kubernetes Naming Requirements**: Kubernetes resources require names that conform to DNS-1123 label standards + +### Examples + +| Git Branch Name | Normalized Name | +|----------------|----------------| +| `feature/new-ui` | `feature-new-ui` | +| `user_story_123` | `user-story-123` | +| `v1.2.3` | `v1-2-3` | +| `adamancini/gh-actions` | `adamancini-gh-actions` | + +This means you can use git branch names directly in task commands without manual transformation: + +```bash +# Works with any git branch name +task customer-create CUSTOMER_NAME=$(git branch --show-current) +task cluster-create CLUSTER_NAME=$(git branch --show-current) +``` + ## Customization Options Common variables that can be overridden: @@ -169,6 +258,12 @@ RELEASE_NOTES="Release notes" # Application configuration APP_SLUG=wg-easy-cre + +# Container registry options +DEV_CONTAINER_REGISTRY=ghcr.io # Default: GitHub Container Registry +# For Google Artifact Registry: +# DEV_CONTAINER_REGISTRY=us-central1-docker.pkg.dev +# DEV_CONTAINER_IMAGE=replicated-qa/wg-easy/wg-easy-tools ``` ## Claude Code Configuration @@ -181,6 +276,40 @@ When using Claude Code with this repository, use these timeout settings for long Example: When running `task helm-install` via Bash tool, use `timeout: 1200000` parameter. +### Early Timeout Detection + +During `helm install` or `helm-install` operations, you can skip waiting for the full timeout if pods end up in the `ImagePullBackOff` state. This indicates image pull failures that won't resolve by waiting longer. Use `kubectl get pods` to check pod status and terminate early if multiple pods show `ImagePullBackOff` or `ErrImagePull` states. + +### Background Monitoring for Helm Operations + +When running any task that calls `helm-install` (including `task helm-install`, `task customer-helm-install`, `task full-test-cycle`, and `task customer-full-test-cycle`), you can monitor the deployment in the background to detect early failures: + +```bash +# In a separate terminal or background process, monitor pod status +watch kubectl get pods --all-namespaces + +# Or check for specific error states +kubectl get pods --all-namespaces --field-selector=status.phase=Failed +kubectl get pods --all-namespaces | grep -E "(ImagePullBackOff|ErrImagePull|CrashLoopBackOff)" +``` + +Common failure patterns that indicate early termination should be considered: +- Multiple pods in `ImagePullBackOff` or `ErrImagePull` states +- Persistent `CrashLoopBackOff` across multiple restarts +- Resource quota exceeded errors +- Persistent volume claim binding failures + +When these conditions are detected, the helm operation can be terminated early rather than waiting for the full timeout period. + +### Local Testing Configuration + +When testing Helm installations locally (including with helmfile), avoid using the `--atomic` flag so that failed resources remain in the cluster for debugging: + +- Remove `atomic: true` from helmfile.yaml.gotmpl during debugging sessions +- Use `helm install` without `--atomic` for manual testing +- Failed pods and resources will persist, allowing inspection with `kubectl describe` and `kubectl logs` +- Clean up manually with `helm uninstall` after debugging is complete + ## Common Workflows ### Local Development @@ -202,18 +331,254 @@ Example: When running `task helm-install` via Bash tool, use `timeout: 1200000` ### Testing a Release -1. Create a customer if needed: `task customer-create CUSTOMER_NAME=test-customer` +#### Option 1: Complete Customer Workflow + +```bash +# Use current git branch name as default for customer and cluster names +# Note: names are automatically normalized (/, _, . replaced with -) by the tasks +task customer-full-test-cycle CUSTOMER_NAME=$(git branch --show-current) CLUSTER_NAME=$(git branch --show-current) +``` + +#### Option 2: Manual Step-by-Step + +1. Create a customer if needed: `task customer-create CUSTOMER_NAME=$(git branch --show-current)` 2. Create a test cluster: `task cluster-create` 3. Set up kubeconfig: `task setup-kubeconfig` 4. Expose ports: `task cluster-ports-expose` -5. Deploy application: `task helm-install` +5. Deploy application: `task customer-helm-install CUSTOMER_NAME=$(git branch --show-current) CLUSTER_NAME=$(git branch --show-current) REPLICATED_LICENSE_ID=xxx CHANNEL_SLUG=$(git branch --show-current)` 6. Run tests: `task test` 7. Clean up: `task cluster-delete` +**Note:** All customer, cluster, and channel names are automatically normalized by replacing `/`, `_`, and `.` characters with `-` to match how slugs are represented in the Replicated Vendor Portal backend and ensure compatibility with Kubernetes naming requirements. + +## Container Registry Setup + +The WG-Easy Image CI workflow publishes container images to three registries for maximum availability: +- **GitHub Container Registry (GHCR)**: `ghcr.io/replicatedhq/platform-examples/wg-easy-tools` +- **Google Artifact Registry (GAR)**: `us-central1-docker.pkg.dev/replicated-qa/wg-easy/wg-easy-tools` +- **Replicated Registry**: `registry.replicated.com/wg-easy-cre/image` + +### Required Secrets + +To enable multi-registry publishing, add these GitHub repository secrets: + +- `GCP_SA_KEY`: Service account JSON key with Artifact Registry Writer permissions +- `WG_EASY_REPLICATED_API_TOKEN`: Replicated vendor portal API token + +### Google Cloud Setup + +1. Create Artifact Registry repository: + +```bash +gcloud artifacts repositories create wg-easy \ + --repository-format=docker \ + --location=us-central1 \ + --project=replicated-qa +``` + +2. Create service account with permissions: + +```bash +gcloud iam service-accounts create github-actions-wg-easy \ + --project=replicated-qa + +gcloud projects add-iam-policy-binding replicated-qa \ + --member="serviceAccount:github-actions-wg-easy@replicated-qa.iam.gserviceaccount.com" \ + --role="roles/artifactregistry.writer" + +gcloud iam service-accounts keys create sa-key.json \ + --iam-account=github-actions-wg-easy@replicated-qa.iam.gserviceaccount.com +``` + +3. Add the `sa-key.json` content as `GCP_SA_KEY` secret in GitHub repository settings. + +### Replicated Registry Setup + +1. Get your Replicated API Token from the vendor portal +2. Add `WG_EASY_REPLICATED_API_TOKEN` as a GitHub repository secret +3. The workflow automatically uses the `replicated` CLI to authenticate with `registry.replicated.com` + +### Using Google Artifact Registry Images + +To use GAR images instead of GHCR: + +```bash +# Set registry to GAR +DEV_CONTAINER_REGISTRY=us-central1-docker.pkg.dev +DEV_CONTAINER_IMAGE=replicated-qa/wg-easy/wg-easy-tools + +# Use GAR image +task dev:start +``` + +## Replicated Registry Proxy + +When deploying in the `replicated` environment, the helmfile automatically configures all container images to use the Replicated Registry proxy for improved performance and reliability. + +### Proxy Configuration + +The proxy automatically rewrites image URLs following this pattern: + +- **Original**: `ghcr.io/wg-easy/wg-easy:14.0` +- **Proxy**: `proxy.replicated.com/proxy/wg-easy-cre/ghcr.io/wg-easy/wg-easy:14.0` + +### Supported Images + +The following images are automatically proxied in the `replicated` environment: + +- **WG-Easy**: `ghcr.io/wg-easy/wg-easy` → `proxy.replicated.com/proxy/wg-easy-cre/ghcr.io/wg-easy/wg-easy` +- **Traefik**: `docker.io/traefik/traefik` → `proxy.replicated.com/proxy/wg-easy-cre/docker.io/traefik/traefik` +- **Cert-Manager**: `quay.io/jetstack/cert-manager-*` → `proxy.replicated.com/proxy/wg-easy-cre/quay.io/jetstack/cert-manager-*` + +### Usage + +The proxy configuration is automatically applied when using the `replicated` environment: + +```bash +# Deploy with proxy (replicated environment) +helmfile -e replicated apply + +# Deploy without proxy (default environment) +helmfile apply +``` + +## GitHub Actions Integration + +The project includes optimized GitHub Actions workflows that leverage the Task-based architecture: + +### PR Validation Workflow +The `wg-easy-pr-validation.yaml` workflow is structured for maximum efficiency: + +1. **Chart Validation** - Uses `task chart-validate` via reusable action +2. **Chart Packaging** - Builds once, shares artifacts between jobs +3. **Release Creation** - Creates Replicated channel and release +4. **Deployment Testing** - Tests full customer workflow +5. **Automatic Cleanup** - Cleans up PR resources + +### Reusable Actions +Located in `.github/actions/` for consistent tool setup and operations: + +- **setup-tools** - Enhanced with improved caching for tools and dependencies +- **chart-validate** - Validates charts using `task chart-validate` +- **chart-package** - Packages charts using `task chart-package-all` +- **replicated-release** - Creates channels and releases using tasks +- **test-deployment** - Complete deployment testing workflow + +### Benefits of Task Integration +- **Consistency** - Same operations work locally and in CI +- **Reduced Duplication** - Charts built once, shared via artifacts +- **Better Caching** - Helm dependencies and tools cached effectively +- **Maintainability** - Logic centralized in Taskfile, not scattered in YAML + +### Usage +PR validation runs automatically on pull requests affecting `applications/wg-easy/`. Manual trigger available via `workflow_dispatch`. + +## Future Considerations + +### Refactoring PR Validation Workflow Using Replicated Actions + +The current GitHub Actions workflow uses custom composite actions that wrap Task-based operations. The [replicated-actions](https://github.com/replicatedhq/replicated-actions) repository provides official actions that could replace several of these custom implementations for improved reliability and reduced maintenance burden. + +#### Current State Analysis + +The current workflow uses custom composite actions: +- `./.github/actions/replicated-release` (uses Task + Replicated CLI) +- `./.github/actions/test-deployment` (complex composite with multiple Task calls) +- Custom cluster and customer management via Task wrappers + +#### Proposed Refactoring Opportunities + +##### 1. Replace Custom Release Creation +**Current**: `./.github/actions/replicated-release` (uses Task + Replicated CLI) +**Replace with**: `replicatedhq/replicated-actions/create-release@v1` + +**Benefits:** +- Official Replicated action with better error handling +- Direct API integration (no Task wrapper needed) +- Built-in airgap build support with configurable timeout +- Outputs channel-slug and release-sequence for downstream jobs + +##### 2. Replace Custom Customer Creation +**Current**: `task customer-create` within test-deployment action +**Replace with**: `replicatedhq/replicated-actions/create-customer@v1` + +**Benefits:** +- Direct customer creation without Task wrapper +- Returns customer-id and license-id as outputs +- Configurable license parameters (expiration, entitlements) +- Better error handling and validation + +##### 3. Replace Custom Cluster Management +**Current**: `task cluster-create` and `task cluster-delete` +**Replace with**: +- `replicatedhq/replicated-actions/create-cluster@v1` +- `replicatedhq/replicated-actions/remove-cluster@v1` + +**Benefits:** +- Direct cluster provisioning without Task wrapper +- Returns cluster-id and kubeconfig as outputs +- More granular configuration options (node groups, instance types) +- Automatic kubeconfig export + +##### 4. Enhance Cleanup Process +**Current**: `task cleanup-pr-resources` +**Replace with**: Individual replicated-actions for cleanup: +- `replicatedhq/replicated-actions/archive-customer@v1` +- `replicatedhq/replicated-actions/remove-cluster@v1` + +**Benefits:** +- More reliable cleanup using official actions +- Better resource tracking via action outputs +- Parallel cleanup operations possible + +##### 5. Simplify Test Deployment Action +**Current**: Large composite action with multiple Task calls +**Refactor to**: Use replicated-actions directly in workflow + +**Benefits:** +- Reduced complexity and maintenance burden +- Better visibility in GitHub Actions UI +- Easier debugging and monitoring +- Consistent error handling across all operations + +#### Implementation Phases + +**Phase 1: Release Creation Refactoring** +- Replace `.github/actions/replicated-release` with direct use of `replicatedhq/replicated-actions/create-release@v1` +- Update workflow to pass chart directory and release parameters directly +- Test release creation functionality + +**Phase 2: Customer and Cluster Management** +- Replace customer creation in test-deployment with `create-customer@v1` +- Replace cluster operations with `create-cluster@v1` +- Update workflow to capture and pass IDs between jobs +- Test customer and cluster provisioning + +**Phase 3: Deployment Testing Simplification** +- Break down test-deployment composite action into individual workflow steps +- Use replicated-actions directly in workflow jobs +- Maintain existing retry logic for cluster creation +- Test end-to-end deployment flow + +**Phase 4: Enhanced Cleanup** +- Replace cleanup task with individual replicated-actions +- Implement parallel cleanup using job matrices +- Add proper error handling for cleanup failures +- Test resource cleanup functionality + +#### Expected Outcomes +- **Reduced Maintenance**: Fewer custom actions to maintain +- **Better Reliability**: Official actions with better error handling +- **Improved Visibility**: Direct action usage in workflow logs +- **Enhanced Features**: Access to advanced features like airgap builds +- **Consistent API Usage**: All operations use official Replicated actions + +This refactoring would maintain the current Task-based local development workflow while leveraging official actions for CI/CD operations, providing the best of both worlds. + ## Additional Resources - [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) \ No newline at end of file +- [Example Patterns](docs/examples.md) diff --git a/applications/wg-easy/Taskfile.yaml b/applications/wg-easy/Taskfile.yaml index ef627383..7e0c198e 100644 --- a/applications/wg-easy/Taskfile.yaml +++ b/applications/wg-easy/Taskfile.yaml @@ -10,6 +10,7 @@ vars: # Release configuration RELEASE_CHANNEL: '{{.RELEASE_CHANNEL | default "Unstable"}}' + RELEASE_CHANNEL_ID: '{{.RELEASE_CHANNEL_ID}}' RELEASE_VERSION: '{{.RELEASE_VERSION | default "0.0.1"}}' RELEASE_NOTES: '{{.RELEASE_NOTES | default "Release created via task release-create"}}' REPLICATED_LICENSE_ID: '{{.REPLICATED_LICENSE_ID}}' @@ -20,7 +21,6 @@ vars: DISK_SIZE: '{{.DISK_SIZE | default "100"}}' INSTANCE_TYPE: '{{.INSTANCE_TYPE | default "r1.small"}}' DISTRIBUTION: '{{.DISTRIBUTION | default "k3s"}}' - KUBECONFIG_FILE: './test-cluster.kubeconfig' # Ports configuration EXPOSE_PORTS: @@ -33,8 +33,14 @@ vars: VM_NAME: '{{.VM_NAME | default (printf "%s-dev" (or (env "GUSER") "user"))}}' # Container workflow configuration + # Available in GitHub Container Registry, Google Artifact Registry, and Replicated Registry DEV_CONTAINER_REGISTRY: '{{.DEV_CONTAINER_REGISTRY | default "ghcr.io"}}' DEV_CONTAINER_IMAGE: '{{.DEV_CONTAINER_IMAGE | default "replicatedhq/platform-examples/wg-easy-tools"}}' + # Alternative registries: + # - Google Artifact Registry: DEV_CONTAINER_REGISTRY=us-central1-docker.pkg.dev DEV_CONTAINER_IMAGE=replicated-qa/wg-easy/wg-easy-tools + # - Replicated Registry: DEV_CONTAINER_REGISTRY=registry.replicated.com DEV_CONTAINER_IMAGE=wg-easy-cre/image + # Container tags: "latest" for main branch, "{branch-name}" for feature branches, semver for releases + # Override with DEV_CONTAINER_TAG=branch-name for feature branch containers or DEV_CONTAINER_TAG=v1.2.3 for releases DEV_CONTAINER_TAG: '{{.DEV_CONTAINER_TAG | default "latest"}}' DEV_CONTAINER_NAME: '{{.DEV_CONTAINER_NAME | default "wg-easy-tools"}}' CONTAINER_RUNTIME: '{{.CONTAINER_RUNTIME | default "podman"}}' @@ -64,37 +70,47 @@ tasks: EMBEDDED: '{{.EMBEDDED | default "false"}}' TIMEOUT: '{{if eq .EMBEDDED "true"}}420{{else}}300{{end}}' TTL: '{{.TTL | default "4h"}}' + # Normalize cluster name by replacing common git branch delimiters with hyphens + # This matches how cluster slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CLUSTER_NAME: + sh: task utils:normalize-name NAME="{{.CLUSTER_NAME}}" status: - | # Check if cluster exists and output info if it does - CLUSTER_INFO=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.CLUSTER_NAME}}")') + CLUSTER_INFO=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CLUSTER_NAME}}")') if [ -n "$CLUSTER_INFO" ]; then - echo "Found existing cluster {{.CLUSTER_NAME}}:" + echo "Found existing cluster {{.NORMALIZED_CLUSTER_NAME}}:" echo "$CLUSTER_INFO" | jq -r '" ID: " + .id + "\n Status: " + .status + "\n Distribution: " + .distribution + "\n Created: " + .created_at + "\n Expires: " + .expires_at' exit 0 fi exit 1 cmds: - | - echo "Creating new cluster {{.CLUSTER_NAME}}..." + echo "Creating new cluster {{.NORMALIZED_CLUSTER_NAME}}..." if [ "{{.EMBEDDED}}" = "true" ]; then - echo "Creating embedded cluster {{.CLUSTER_NAME}} with license ID {{.REPLICATED_LICENSE_ID}}..." - replicated cluster create --distribution embedded-cluster --name {{.CLUSTER_NAME}} --license-id {{.REPLICATED_LICENSE_ID}} --ttl {{.TTL}} + echo "Creating embedded cluster {{.NORMALIZED_CLUSTER_NAME}} with license ID {{.REPLICATED_LICENSE_ID}}..." + replicated cluster create --distribution embedded-cluster --name {{.NORMALIZED_CLUSTER_NAME}} --license-id {{.REPLICATED_LICENSE_ID}} --ttl {{.TTL}} 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}} --ttl {{.TTL}} + echo "Creating cluster {{.NORMALIZED_CLUSTER_NAME}} with distribution {{.DISTRIBUTION}}..." + replicated cluster create --name {{.NORMALIZED_CLUSTER_NAME}} --distribution {{.DISTRIBUTION}} --version {{.K8S_VERSION}} --disk {{.DISK_SIZE}} --instance-type {{.INSTANCE_TYPE}} --ttl {{.TTL}} fi - task: utils:wait-for-cluster vars: TIMEOUT: "{{.TIMEOUT}}" + CLUSTER_NAME: "{{.NORMALIZED_CLUSTER_NAME}}" cluster-list: desc: List the cluster + vars: + # Normalize cluster name by replacing common git branch delimiters with hyphens + # This matches how cluster slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CLUSTER_NAME: + sh: task utils:normalize-name NAME="{{.CLUSTER_NAME}}" 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)" + CLUSTER_ID=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CLUSTER_NAME}}") | .id') + EXPIRES=$(replicated cluster ls --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CLUSTER_NAME}}") | .expires_at') + echo "{{.NORMALIZED_CLUSTER_NAME}} Cluster ID: ($CLUSTER_ID) Expires: ($EXPIRES)" test: desc: Run a basic test suite @@ -107,6 +123,8 @@ tasks: verify-kubeconfig: desc: Verify kubeconfig run: once + vars: + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .CLUSTER_NAME)}}' cmds: - | if [ -f {{.KUBECONFIG_FILE}} ]; then @@ -124,9 +142,19 @@ tasks: setup-kubeconfig: desc: Get kubeconfig and prepare cluster for application deployment run: once + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME | default .CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .CLUSTER_NAME)}}' cmds: - task: utils:get-kubeconfig + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE}}' - task: utils:remove-k3s-traefik + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE}}' + - echo "{{.KUBECONFIG_FILE}}" status: - | # Check if kubeconfig exists @@ -142,10 +170,38 @@ tasks: - cluster-create - verify-kubeconfig + helm-repo-add: + desc: Add all HTTP/HTTPS Helm repositories found in Chart.yaml files + silent: false + run: once + cmds: + - echo "Adding Helm repositories from Chart.yaml files..." + - | + # Find all Chart.yaml files and extract HTTP/HTTPS repositories + for chart_file in $(find charts/ -maxdepth 2 -name "Chart.yaml"); do + echo "Processing $chart_file" + + # Extract repository URLs that start with http:// or https:// + yq eval '.dependencies[]?.repository' "$chart_file" 2>/dev/null | grep -E '^https?://' | while read -r repo_url; do + if [ -n "$repo_url" ]; then + # Generate a repository name from the URL + repo_name=$(echo "$repo_url" | sed 's|https\?://||' | sed 's|[./]|-|g' | sed 's|-*$||') + + echo "Adding repository: $repo_name -> $repo_url" + helm repo add "$repo_name" "$repo_url" || echo "Repository $repo_name may already exist" + fi + done + done + - echo "Updating Helm repository index..." + - helm repo update + - echo "All Helm repositories added and updated!" + dependencies-update: desc: Update Helm dependencies for all charts run: once cmds: + - echo "Ensure Helm credentials are cleared..." + - helm registry logout registry.replicated.com || true - echo "Updating Helm dependencies for all charts..." - | # Find all charts and update their dependencies @@ -154,6 +210,8 @@ tasks: helm dependency update --skip-refresh "$chart_dir" done - echo "All dependencies updated!" + deps: + - helm-repo-add cluster-ports-expose: desc: Expose configured ports for a cluster and capture exposed URLs @@ -187,6 +245,7 @@ tasks: desc: Run preflight checks on Helm charts using preflight CLI (use DRY_RUN=true for dry-run) vars: DRY_RUN: '{{.DRY_RUN | default "false"}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .CLUSTER_NAME)}}' cmds: - | PREFLIGHT_FLAGS="" @@ -196,7 +255,7 @@ tasks: for chart_dir in $(find charts/ -maxdepth 2 -name "Chart.yaml" | xargs dirname); do echo "Running preflight on $chart_dir" - helm template $chart_dir | kubectl preflight - $PREFLIGHT_FLAGS + KUBECONFIG={{.KUBECONFIG_FILE}} helm template $chart_dir | KUBECONFIG={{.KUBECONFIG_FILE}} kubectl preflight - $PREFLIGHT_FLAGS done deps: - setup-kubeconfig @@ -205,6 +264,10 @@ tasks: desc: Install all charts using helmfile vars: HELM_ENV: '{{.HELM_ENV | default "default"}}' + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .CLUSTER_NAME)}}' + REPLICATED_LICENSE_ID: '{{.REPLICATED_LICENSE_ID}}' + CHANNEL: '{{.CHANNEL}}' cmds: - echo "Installing all charts via helmfile" - | @@ -220,7 +283,14 @@ tasks: # Deploy with helmfile echo "Using $ENV_VARS" - eval "KUBECONFIG={{.KUBECONFIG_FILE}} HELMFILE_ENVIRONMENT={{.HELM_ENV}} REPLICATED_APP={{.APP_SLUG}} $ENV_VARS helmfile sync --wait" + + if [ "{{.HELM_ENV}}" = "replicated" ]; then + HELM_CMD="KUBECONFIG='{{.KUBECONFIG_FILE}}' HELMFILE_ENVIRONMENT='{{.HELM_ENV}}' APP_SLUG='{{.APP_SLUG}}' REPLICATED_LICENSE_ID='{{.REPLICATED_LICENSE_ID}}' CHANNEL='{{.CHANNEL}}' $ENV_VARS helmfile sync --wait" + eval "$HELM_CMD" + else + HELM_CMD="KUBECONFIG='{{.KUBECONFIG_FILE}}' HELMFILE_ENVIRONMENT='{{.HELM_ENV}}' APP_SLUG='{{.APP_SLUG}}' $ENV_VARS helmfile sync --wait" + eval "$HELM_CMD" + fi - echo "All charts installed!" deps: - setup-kubeconfig @@ -228,6 +298,9 @@ tasks: helm-uninstall: desc: Uninstall all charts using helm uninstall + silent: false + vars: + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .CLUSTER_NAME)}}' cmds: - echo "Uninstalling all charts via helm" - | @@ -249,12 +322,19 @@ tasks: cluster-delete: desc: Delete all test clusters with matching name and clean up kubeconfig + silent: false + vars: + # Normalize cluster name by replacing common git branch delimiters with hyphens + # This matches how cluster slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CLUSTER_NAME: + sh: task utils:normalize-name NAME="{{.CLUSTER_NAME}}" + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .NORMALIZED_CLUSTER_NAME)}}' cmds: - - echo "Deleting clusters named {{.CLUSTER_NAME}}..." + - echo "Deleting clusters named {{.NORMALIZED_CLUSTER_NAME}}..." - | - CLUSTER_IDS=$(replicated cluster ls | grep "{{.CLUSTER_NAME}}" | awk '{print $1}') + CLUSTER_IDS=$(replicated cluster ls | grep "{{.NORMALIZED_CLUSTER_NAME}}" | awk '{print $1}') if [ -z "$CLUSTER_IDS" ]; then - echo "No clusters found with name {{.CLUSTER_NAME}}" + echo "No clusters found with name {{.NORMALIZED_CLUSTER_NAME}}" exit 0 fi @@ -335,21 +415,33 @@ tasks: - dependencies-update release-create: - desc: Create and promote a release using the Replicated CLI + desc: Create and promote a release using the Replicated CLI (supports both channel names and IDs) run: once vars: RELEASE_CHANNEL: '{{.RELEASE_CHANNEL | default "Unstable"}}' + RELEASE_CHANNEL_ID: '{{.RELEASE_CHANNEL_ID}}' RELEASE_VERSION: '{{.RELEASE_VERSION | default "0.0.1"}}' RELEASE_NOTES: '{{.RELEASE_NOTES | default "Release created via task release-create"}}' + # Use channel ID if provided, otherwise fall back to channel name + CHANNEL_TARGET: '{{if .RELEASE_CHANNEL_ID}}{{.RELEASE_CHANNEL_ID}}{{else}}{{.RELEASE_CHANNEL}}{{end}}' requires: - vars: [APP_SLUG, RELEASE_CHANNEL, RELEASE_VERSION] + vars: [APP_SLUG, RELEASE_VERSION] cmds: - - echo "Creating and promoting release for {{.APP_SLUG}} to channel {{.RELEASE_CHANNEL}}..." + - | + if [ -n "{{.RELEASE_CHANNEL_ID}}" ]; then + echo "Creating and promoting release for {{.APP_SLUG}} to channel ID {{.RELEASE_CHANNEL_ID}}..." + else + echo "Creating and promoting release for {{.APP_SLUG}} to channel {{.RELEASE_CHANNEL}}..." + fi - | # Create and promote the release in one step echo "Creating release from files in ./release directory..." - replicated release create --app {{.APP_SLUG}} --yaml-dir ./release --release-notes "{{.RELEASE_NOTES}}" --promote {{.RELEASE_CHANNEL}} --version {{.RELEASE_VERSION}} - echo "Release version {{.RELEASE_VERSION}} created and promoted to channel {{.RELEASE_CHANNEL}}" + replicated release create --app {{.APP_SLUG}} --yaml-dir ./release --release-notes "{{.RELEASE_NOTES}}" --promote {{.CHANNEL_TARGET}} --version {{.RELEASE_VERSION}} + if [ -n "{{.RELEASE_CHANNEL_ID}}" ]; then + echo "Release version {{.RELEASE_VERSION}} created and promoted to channel ID {{.RELEASE_CHANNEL_ID}}" + else + echo "Release version {{.RELEASE_VERSION}} created and promoted to channel {{.RELEASE_CHANNEL}}" + fi deps: - release-prepare @@ -360,31 +452,45 @@ tasks: CUSTOMER_NAME: '{{.CUSTOMER_NAME | default "test-customer"}}' CUSTOMER_EMAIL: '{{.CUSTOMER_EMAIL | default "test@example.com"}}' RELEASE_CHANNEL: '{{.RELEASE_CHANNEL | default "Unstable"}}' + RELEASE_CHANNEL_ID: '{{.RELEASE_CHANNEL_ID}}' LICENSE_TYPE: '{{.LICENSE_TYPE | default "dev"}}' EXPIRES_IN: '{{.EXPIRES_IN | default ""}}' + # Normalize customer name by replacing common git branch delimiters with hyphens + # This matches how customer slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CUSTOMER_NAME: + sh: task utils:normalize-name NAME="{{.CUSTOMER_NAME}}" requires: vars: [APP_SLUG] cmds: - | # First check if customer already exists - echo "Looking for existing customer {{.CUSTOMER_NAME}} for app {{.APP_SLUG}}..." - EXISTING_CUSTOMER=$(replicated customer ls --app {{.APP_SLUG}} --output json | jq -r '.[] | select(.name=="{{.CUSTOMER_NAME}}") | .id' | head -1) + echo "Looking for existing customer {{.NORMALIZED_CUSTOMER_NAME}} for app {{.APP_SLUG}}..." + EXISTING_CUSTOMER=$(replicated customer ls --app {{.APP_SLUG}} --output json | jq -r 'if type == "array" then .[] | select(.name=="{{.NORMALIZED_CUSTOMER_NAME}}") | .id else empty end' 2>/dev/null | head -1) - if [ -n "$EXISTING_CUSTOMER" ]; then - echo "Found existing customer {{.CUSTOMER_NAME}} with ID: $EXISTING_CUSTOMER" + if [ -n "$EXISTING_CUSTOMER" ] && [ "$EXISTING_CUSTOMER" != "null" ]; then + echo "Found existing customer {{.NORMALIZED_CUSTOMER_NAME}} with ID: $EXISTING_CUSTOMER" echo "$EXISTING_CUSTOMER" exit 0 fi # No existing customer found, create a new one - echo "Creating new customer {{.CUSTOMER_NAME}} for app {{.APP_SLUG}}..." + echo "Creating new customer {{.NORMALIZED_CUSTOMER_NAME}} for app {{.APP_SLUG}}..." + + # Determine which channel parameter to use (--channel accepts both names and IDs) + if [ -n "{{.RELEASE_CHANNEL_ID}}" ]; then + CHANNEL_PARAM="--channel {{.RELEASE_CHANNEL_ID}}" + echo "Using channel ID: {{.RELEASE_CHANNEL_ID}}" + else + CHANNEL_PARAM="--channel {{.RELEASE_CHANNEL}}" + echo "Using channel name: {{.RELEASE_CHANNEL}}" + fi # Build the command with optional expiration CMD="replicated customer create \ --app {{.APP_SLUG}} \ - --name {{.CUSTOMER_NAME}} \ + --name {{.NORMALIZED_CUSTOMER_NAME}} \ --email {{.CUSTOMER_EMAIL}} \ - --channel {{.RELEASE_CHANNEL}} \ + $CHANNEL_PARAM \ --type {{.LICENSE_TYPE}} \ --output json" @@ -394,7 +500,7 @@ tasks: fi # Create the customer and capture the output - CUSTOMER_JSON=$($CMD) + CUSTOMER_JSON=$(eval $CMD) # Extract and output just the customer ID echo "$CUSTOMER_JSON" | jq -r '.id' @@ -488,6 +594,120 @@ tasks: # Confirm archiving echo "Customer '$CUSTOMER_NAME' (ID: {{.CUSTOMER_ID}}) successfully archived" + channel-create: + desc: Create a Replicated release channel and return its ID + silent: false + vars: + RELEASE_CHANNEL: '{{.RELEASE_CHANNEL}}' + # Normalize channel name by replacing common git branch delimiters with hyphens + # This matches how channel slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_RELEASE_CHANNEL: + sh: task utils:normalize-name NAME="{{.RELEASE_CHANNEL}}" + requires: + vars: [APP_SLUG, RELEASE_CHANNEL] + cmds: + - echo "Creating channel {{.NORMALIZED_RELEASE_CHANNEL}} for app {{.APP_SLUG}}..." + - | + # Check if channel already exists + echo "Debug: Checking existing channels for app {{.APP_SLUG}}..." + CHANNEL_LIST_RESPONSE=$(replicated channel ls --app {{.APP_SLUG}} --output json) + echo "Debug: Channel list response: $CHANNEL_LIST_RESPONSE" + EXISTING_CHANNEL_ID=$(echo "$CHANNEL_LIST_RESPONSE" | jq -r 'if type == "array" then .[] | select(.name=="{{.NORMALIZED_RELEASE_CHANNEL}}") | .id else empty end' 2>/dev/null | head -1) + + if [ -n "$EXISTING_CHANNEL_ID" ] && [ "$EXISTING_CHANNEL_ID" != "null" ]; then + echo "Channel {{.NORMALIZED_RELEASE_CHANNEL}} already exists for app {{.APP_SLUG}} with ID: $EXISTING_CHANNEL_ID" + echo "$EXISTING_CHANNEL_ID" + exit 0 + fi + + # Create the channel and capture its ID + CHANNEL_OUTPUT=$(replicated channel create --app {{.APP_SLUG}} --name {{.NORMALIZED_RELEASE_CHANNEL}} --output json) + CHANNEL_ID=$(echo "$CHANNEL_OUTPUT" | jq -r '.id') + echo "Channel {{.NORMALIZED_RELEASE_CHANNEL}} created successfully with ID: $CHANNEL_ID" + echo "$CHANNEL_ID" + + channel-delete: + desc: Archive a Replicated release channel by ID + silent: false + vars: + RELEASE_CHANNEL_ID: '{{.RELEASE_CHANNEL_ID}}' + requires: + vars: [APP_SLUG, RELEASE_CHANNEL_ID] + cmds: + - echo "Archiving channel ID {{.RELEASE_CHANNEL_ID}} for app {{.APP_SLUG}}..." + - | + # Get channel name for logging + CHANNEL_NAME=$(replicated channel ls --app {{.APP_SLUG}} --output json | jq -r 'if type == "array" then .[] | select(.id=="{{.RELEASE_CHANNEL_ID}}") | .name else empty end' 2>/dev/null | head -1) + + if [ -z "$CHANNEL_NAME" ] || [ "$CHANNEL_NAME" = "null" ]; then + echo "Error: Channel ID {{.RELEASE_CHANNEL_ID}} not found for app {{.APP_SLUG}}" + exit 1 + fi + + # Archive the channel + replicated channel archive --app {{.APP_SLUG}} {{.RELEASE_CHANNEL_ID}} + echo "Channel $CHANNEL_NAME (ID: {{.RELEASE_CHANNEL_ID}}) archived successfully" + + chart-lint-all: + desc: Lint all Helm charts in the project + run: once + cmds: + - echo "Linting all Helm charts..." + - | + # Find all charts and lint them + for chart_dir in $(find charts/ -maxdepth 2 -name "Chart.yaml" | xargs dirname); do + echo "Linting chart: $chart_dir" + helm lint "$chart_dir" + done + - echo "All charts linted successfully!" + deps: + - dependencies-update + + chart-template-all: + desc: Template all Helm charts to validate syntax + run: once + cmds: + - echo "Templating all Helm charts..." + - | + # Find all charts and template them + for chart_dir in $(find charts/ -maxdepth 2 -name "Chart.yaml" | xargs dirname); do + echo "Templating chart: $chart_dir" + helm template test-release "$chart_dir" --dry-run >/dev/null + done + - echo "All charts templated successfully!" + deps: + - dependencies-update + + chart-validate: + desc: Validate all Helm charts (lint + template + helmfile) + cmds: + - task: chart-lint-all + - task: chart-template-all + - echo "Validating helmfile template..." + - | + if [ -f "helmfile.yaml.gotmpl" ]; then + # Set required environment variables for helmfile validation + export REPLICATED_APP="test-app" + export CHANNEL="test-channel" + export REPLICATED_LICENSE_ID="test-license" + export TF_EXPOSED_URL="test.example.com" + export HELMFILE_ENVIRONMENT="default" + + echo "Building helmfile template..." + helmfile build >/dev/null + echo "Helmfile template validation successful!" + else + echo "No helmfile.yaml.gotmpl found, skipping helmfile validation" + fi + + chart-package-all: + desc: Package all Helm charts for distribution + cmds: + - echo "Packaging all Helm charts..." + - task: dependencies-update + - task: release-prepare + - echo "All charts packaged successfully!" + clean: desc: Remove temporary Helm directories, chart dependencies, and release folder cmds: @@ -515,6 +735,91 @@ tasks: find . -type d -name "tmpcharts-*" -exec rm -rf {} \; 2>/dev/null || true - echo "Cleaning complete!" + pr-validation-cycle: + desc: Complete PR validation workflow (validate charts, create release, test deployment) + vars: + BRANCH_NAME: '{{.BRANCH_NAME | default "pr-test"}}' + CHANNEL_NAME: '{{.CHANNEL_NAME | default .BRANCH_NAME}}' + # Normalize names by replacing common git branch delimiters with hyphens + # This matches how slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_BRANCH_NAME: + sh: task utils:normalize-name NAME="{{.BRANCH_NAME}}" + NORMALIZED_CHANNEL_NAME: + sh: task utils:normalize-name NAME="{{.CHANNEL_NAME}}" + CHANNEL_ID: + sh: task channel-create RELEASE_CHANNEL={{.NORMALIZED_CHANNEL_NAME}} + requires: + vars: [BRANCH_NAME] + cmds: + - echo "Starting PR validation cycle for branch {{.NORMALIZED_BRANCH_NAME}}" + - echo "Step 1 - Validating charts..." + - task: chart-validate + - echo "Step 2 - Building and creating release..." + - task: release-create + vars: + RELEASE_CHANNEL: "{{.NORMALIZED_CHANNEL_NAME}}" + - echo "Step 3 - Testing deployment..." + - task: customer-create + vars: + CUSTOMER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + RELEASE_CHANNEL_ID: "{{.CHANNEL_ID}}" + - task: cluster-create + vars: + CLUSTER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + - task: setup-kubeconfig + vars: + CLUSTER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + - task: cluster-ports-expose + vars: + CLUSTER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + - task: customer-helm-install + vars: + CUSTOMER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + CLUSTER_NAME: "{{.NORMALIZED_BRANCH_NAME}}" + CHANNEL_ID: "{{.CHANNEL_ID}}" + REPLICATED_LICENSE_ID: + sh: task utils:get-customer-license CUSTOMER_NAME={{.NORMALIZED_BRANCH_NAME}} + - task: test + - echo "PR validation cycle completed successfully!" + + cleanup-pr-resources: + desc: Cleanup PR-related resources (clusters, customers, channels) + vars: + BRANCH_NAME: '{{.BRANCH_NAME | default "pr-test"}}' + CHANNEL_NAME: '{{.CHANNEL_NAME | default .BRANCH_NAME}}' + # Normalize names by replacing common git branch delimiters with hyphens + # This matches how slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_BRANCH_NAME: + sh: task utils:normalize-name NAME="{{.BRANCH_NAME}}" + NORMALIZED_CHANNEL_NAME: + sh: task utils:normalize-name NAME="{{.CHANNEL_NAME}}" + requires: + vars: [BRANCH_NAME] + cmds: + - echo "Cleaning up PR resources for branch {{.NORMALIZED_BRANCH_NAME}}" + - echo "Deleting cluster..." + - | + task cluster-delete CLUSTER_NAME="{{.NORMALIZED_BRANCH_NAME}}" || echo "Cluster deletion failed or cluster not found" + - echo "Archiving customer..." + - | + CUSTOMER_ID=$(replicated customer ls --app {{.APP_SLUG}} --output json | jq -r 'if type == "array" then .[] | select(.name=="{{.NORMALIZED_BRANCH_NAME}}") | .id else empty end' 2>/dev/null | head -1) + if [ -n "$CUSTOMER_ID" ] && [ "$CUSTOMER_ID" != "null" ]; then + task customer-delete CUSTOMER_ID="$CUSTOMER_ID" || echo "Customer deletion failed" + else + echo "No customer found with name {{.NORMALIZED_BRANCH_NAME}}" + fi + - echo "Archiving channel..." + - | + # Get channel ID and delete it + CHANNEL_ID=$(replicated channel ls --app {{.APP_SLUG}} --output json | jq -r 'if type == "array" then .[] | select(.name=="{{.NORMALIZED_CHANNEL_NAME}}") | .id else empty end' 2>/dev/null | head -1) + if [ -n "$CHANNEL_ID" ] && [ "$CHANNEL_ID" != "null" ]; then + task channel-delete RELEASE_CHANNEL_ID="$CHANNEL_ID" || echo "Channel deletion failed" + else + echo "No channel found with name {{.NORMALIZED_CHANNEL_NAME}}" + fi + + - echo "PR resource cleanup completed!" + full-test-cycle: desc: Create cluster, get kubeconfig, expose ports, update dependencies, deploy charts, test, and delete, and clean up build artifacts cmds: @@ -527,6 +832,111 @@ tasks: - task: test - task: cluster-delete + customer-helm-install: + desc: Deploy charts using replicated environment with customer license and channel + vars: + CUSTOMER_NAME: '{{.CUSTOMER_NAME}}' + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + REPLICATED_LICENSE_ID: '{{.REPLICATED_LICENSE_ID}}' + CHANNEL_SLUG: '{{.CHANNEL_SLUG}}' + CHANNEL_ID: '{{.CHANNEL_ID}}' + # Normalize names by replacing common git branch delimiters with hyphens + # This matches how slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CUSTOMER_NAME: + sh: task utils:normalize-name NAME="{{.CUSTOMER_NAME}}" + NORMALIZED_CLUSTER_NAME: + sh: task utils:normalize-name NAME="{{.CLUSTER_NAME}}" + NORMALIZED_CHANNEL_SLUG: + sh: task utils:normalize-name NAME="{{.CHANNEL_SLUG}}" + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE | default (printf "./%s.kubeconfig" .NORMALIZED_CLUSTER_NAME)}}' + requires: + vars: [CUSTOMER_NAME, CLUSTER_NAME, REPLICATED_LICENSE_ID] + cmds: + - echo "Deploying charts for customer {{.NORMALIZED_CUSTOMER_NAME}} using replicated environment..." + - echo "Cluster:{{.NORMALIZED_CLUSTER_NAME}}" + - | + # Determine channel identifier to use and log it + if [ -n "{{.CHANNEL_ID}}" ]; then + echo "Channel ID:{{.CHANNEL_ID}}" + CHANNEL_PARAM="{{.CHANNEL_ID}}" + else + echo "Channel Slug:{{.NORMALIZED_CHANNEL_SLUG}}" + CHANNEL_PARAM="{{.NORMALIZED_CHANNEL_SLUG}}" + fi + echo "License ID:{{.REPLICATED_LICENSE_ID}}" + - | + # Get customer email for registry authentication + echo "Getting customer email for registry authentication..." + CUSTOMER_EMAIL=$(replicated customer inspect --customer $(replicated customer ls --app "{{.APP_SLUG}}" --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CUSTOMER_NAME}}") | .id') --app "{{.APP_SLUG}}" | grep "EMAIL:" | awk '{print $2}') + echo "Customer email: $CUSTOMER_EMAIL" + + # Authenticate with Replicated registry using customer email and license ID + echo "Authenticating with Replicated registry..." + echo "{{.REPLICATED_LICENSE_ID}}" | helm registry login registry.replicated.com --username "$CUSTOMER_EMAIL" --password-stdin + - | + # Determine which channel parameter to use for helm install + if [ -n "{{.CHANNEL_ID}}" ]; then + CHANNEL_PARAM="{{.CHANNEL_ID}}" + else + CHANNEL_PARAM="{{.NORMALIZED_CHANNEL_SLUG}}" + fi + + # Deploy using replicated environment with customer-specific settings + task helm-install HELM_ENV=replicated REPLICATED_LICENSE_ID="{{.REPLICATED_LICENSE_ID}}" CHANNEL="$CHANNEL_PARAM" KUBECONFIG_FILE="{{.KUBECONFIG_FILE}}" CLUSTER_NAME="{{.NORMALIZED_CLUSTER_NAME}}" + - echo "Customer helm install complete for {{.NORMALIZED_CUSTOMER_NAME}}" + + customer-full-test-cycle: + desc: Complete customer workflow - create cluster, find customer, deploy using existing releases, test (no cleanup for CD) + vars: + CUSTOMER_NAME: '{{.CUSTOMER_NAME}}' + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + # Normalize names by replacing common git branch delimiters with hyphens + # This matches how slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CUSTOMER_NAME: + sh: task utils:normalize-name NAME="{{.CUSTOMER_NAME}}" + NORMALIZED_CLUSTER_NAME: + sh: task utils:normalize-name NAME="{{.CLUSTER_NAME}}" + requires: + vars: [CUSTOMER_NAME, CLUSTER_NAME] + cmds: + - echo "Starting customer full test cycle..." + - echo "Customer:{{.NORMALIZED_CUSTOMER_NAME}}" + - echo "Cluster:{{.NORMALIZED_CLUSTER_NAME}}" + + # Setup cluster infrastructure + - task: cluster-create + vars: + CLUSTER_NAME: '{{.NORMALIZED_CLUSTER_NAME}}' + - task: setup-kubeconfig + vars: + CLUSTER_NAME: '{{.NORMALIZED_CLUSTER_NAME}}' + - task: cluster-ports-expose + vars: + CLUSTER_NAME: '{{.NORMALIZED_CLUSTER_NAME}}' + # - task: dependencies-update + + # Setup customer and get license (use existing releases) + - echo "Creating/finding customer {{.NORMALIZED_CUSTOMER_NAME}}..." + - task: customer-create + vars: + CUSTOMER_NAME: '{{.NORMALIZED_CUSTOMER_NAME}}' + - echo "Getting license ID and channel for customer {{.NORMALIZED_CUSTOMER_NAME}}..." + - task: customer-helm-install + vars: + CUSTOMER_NAME: '{{.NORMALIZED_CUSTOMER_NAME}}' + CLUSTER_NAME: '{{.NORMALIZED_CLUSTER_NAME}}' + REPLICATED_LICENSE_ID: + sh: task utils:get-customer-license CUSTOMER_NAME={{.NORMALIZED_CUSTOMER_NAME}} + CHANNEL_ID: + sh: replicated customer ls --app {{.APP_SLUG}} --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CUSTOMER_NAME}}") | .channels[0].channelId' + + # Run tests + - task: test + + - echo "Customer full test cycle complete! Environment left running for continuous deployment." + - echo "Cluster:{{.NORMALIZED_CLUSTER_NAME}}" + - echo "Customer:{{.NORMALIZED_CUSTOMER_NAME}}" + cmx-vm-create: desc: Create a CMX VM instance using Replicated CLI run: once @@ -559,7 +969,7 @@ tasks: - | echo "Deleting CMX VM {{.CMX_VM_NAME}}..." replicated vm rm {{.CMX_VM_NAME}} - + cmx-vm-install: desc: Download and install the app as Embedded Cluster on CMX VM requires: @@ -616,7 +1026,7 @@ tasks: echo 'Extracting installer...' tar -xvzf {{.APP_SLUG}}-{{.CHANNEL}}.tgz - + echo "Binary is available at ./{{.APP_SLUG}}" EOF @@ -644,40 +1054,47 @@ tasks: fi airgap-build: - desc: Check and build airgap bundle for the latest release + desc: Check and build airgap bundle for the latest release silent: true cmds: - | echo "Checking if airgap build is available for latest release in channel {{.RELEASE_CHANNEL}}..." - + # Get release list and extract app ID and channel ID RELEASE_DATA=$(replicated release ls -o json) APP_ID=$(echo "$RELEASE_DATA" | jq -r '.[0].appId') - CHANNEL_ID=$(echo "$RELEASE_DATA" | jq -r '.[0].activeChannels[] | select(.name == "{{.RELEASE_CHANNEL}}") | .id') - + # Try to get channel ID from parameter first, fall back to channel name lookup + if [ -n "{{.RELEASE_CHANNEL_ID}}" ]; then + CHANNEL_ID="{{.RELEASE_CHANNEL_ID}}" + echo "Using provided channel ID: $CHANNEL_ID" + else + CHANNEL_ID=$(echo "$RELEASE_DATA" | jq -r '.[0].activeChannels[] | select(.name == "{{.RELEASE_CHANNEL}}") | .id') + echo "Looked up channel ID for {{.RELEASE_CHANNEL}}: $CHANNEL_ID" + fi + if [ -z "$APP_ID" ] || [ "$APP_ID" = "null" ]; then echo "Error: Could not retrieve app ID from latest releases" exit 1 fi - + if [ -z "$CHANNEL_ID" ] || [ "$CHANNEL_ID" = "null" ]; then echo "Error: Could not find channel ID for channel {{.RELEASE_CHANNEL}}" exit 1 fi - + echo "Found app ID: $APP_ID, channel ID: $CHANNEL_ID" - + # Get channel releases and check airgap build status CHANNEL_RELEASES=$(replicated api get "v3/app/$APP_ID/channel/$CHANNEL_ID/releases") AIRGAP_BUILD_STATUS=$(echo "$CHANNEL_RELEASES" | jq -r '.releases[0].airgapBuildStatus // "none"') AIRGAP_BUILD_ERROR=$(echo "$CHANNEL_RELEASES" | jq -r '.releases[0].airgapBuildError // "none"') AIRGAP_BUNDLE_IMAGES=$(echo "$CHANNEL_RELEASES" | jq -r '.releases[0].airgapBundleImages // "none"') AIRGAP_LATEST_SEQUENCE=$(echo "$CHANNEL_RELEASES" | jq -r '.releases[0].channelSequence') - + echo "Airgap build status: $AIRGAP_BUILD_STATUS" if [ "$AIRGAP_BUILD_STATUS" = "built" ]; then - echo "Airgap is already buit for sequence $AIRGAP_LATEST_SEQUENCE" + echo "Airgap is already built for sequence $AIRGAP_LATEST_SEQUENCE" echo "Airgap bundle images: $AIRGAP_BUNDLE_IMAGES" exit 0 fi @@ -710,4 +1127,4 @@ tasks: echo "Timeout: Airgap build did not complete within 5 minutes." echo "Last build status: $AIRGAP_BUILD_STATUS" echo "Last build error: $AIRGAP_BUILD_ERROR" - exit 1 \ No newline at end of file + exit 1 diff --git a/applications/wg-easy/charts/cert-manager/Chart.lock b/applications/wg-easy/charts/cert-manager/Chart.lock index 90b49255..6fcc121d 100644 --- a/applications/wg-easy/charts/cert-manager/Chart.lock +++ b/applications/wg-easy/charts/cert-manager/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: v1.14.5 - name: templates repository: file://../templates - version: 1.0.0 -digest: sha256:ab86a335f7f473446968c607ed7920bf4ce29f625e5ff6175be17bb2e1101a32 -generated: "2025-05-06T15:35:47.871225-04:00" + version: 1.1.0 +digest: sha256:e86e690bcaff2f6d914e0ec7c23f9eafbbb9b2a92324d882e164597345a5ae16 +generated: "2025-06-25T10:58:31.760745-04:00" diff --git a/applications/wg-easy/charts/replicated/Chart.lock b/applications/wg-easy/charts/replicated/Chart.lock index 2719b739..8ce12e03 100644 --- a/applications/wg-easy/charts/replicated/Chart.lock +++ b/applications/wg-easy/charts/replicated/Chart.lock @@ -1,9 +1,9 @@ dependencies: - name: templates repository: file://../templates - version: 1.0.0 + version: 1.1.0 - name: replicated repository: oci://registry.replicated.com/library - version: 1.5.3 -digest: sha256:35588c7f070f319202e6194bd952aa4f4195336e6880855076860acfd7fd1736 -generated: "2025-05-15T13:31:37.79846+01:00" + version: 1.7.0 +digest: sha256:846ea61ba3696e1ba9b6283a30b39754558750c1ff9c779981595cd592259501 +generated: "2025-06-25T10:58:27.696287-04:00" diff --git a/applications/wg-easy/charts/replicated/Chart.yaml b/applications/wg-easy/charts/replicated/Chart.yaml index 6433f996..6fb9e788 100644 --- a/applications/wg-easy/charts/replicated/Chart.yaml +++ b/applications/wg-easy/charts/replicated/Chart.yaml @@ -1,5 +1,5 @@ name: replicated -version: 1.0.0 +version: 1.7.0 apiVersion: v2 dependencies: - name: templates @@ -7,4 +7,4 @@ dependencies: repository: file://../templates - name: replicated repository: oci://registry.replicated.com/library - version: 1.5.3 + version: 1.7.0 diff --git a/applications/wg-easy/charts/templates/Chart.yaml b/applications/wg-easy/charts/templates/Chart.yaml index ff801ee9..b2e1a965 100644 --- a/applications/wg-easy/charts/templates/Chart.yaml +++ b/applications/wg-easy/charts/templates/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 appVersion: latest description: Common templates name: templates -version: 1.0.0 +version: 1.1.0 kubeVersion: ">=1.16.0-0" diff --git a/applications/wg-easy/charts/templates/templates/imagepullsecret.yaml b/applications/wg-easy/charts/templates/templates/imagepullsecret.yaml new file mode 100644 index 00000000..b10e6fd0 --- /dev/null +++ b/applications/wg-easy/charts/templates/templates/imagepullsecret.yaml @@ -0,0 +1,12 @@ +{{ if dig "replicated" "imagePullSecret" "enabled" false .Values.AsMap }} +apiVersion: v1 +kind: Secret +metadata: + # Note: Do not use "replicated" for the name of the pull secret + name: replicated-pull-secret + namespace: {{ .Release.Namespace }} +type: kubernetes.io/dockerconfigjson +data: + # dockerconfigjson from Replicated Helm CLI installs is already a base64 encoded string + .dockerconfigjson: {{ .Values.global.replicated.dockerconfigjson }} +{{ end }} diff --git a/applications/wg-easy/charts/templates/values.yaml b/applications/wg-easy/charts/templates/values.yaml index 9340364a..67cdae7b 100644 --- a/applications/wg-easy/charts/templates/values.yaml +++ b/applications/wg-easy/charts/templates/values.yaml @@ -1,4 +1,4 @@ -#traefikRoutes: +# traefikRoutes: # host.example.com: # serviceName: my-serviceName # servicePort: my-servicePort @@ -21,7 +21,10 @@ # - pathPrefix: /docs # auth: true # traefikRouteTCP: -# - serviceName: -# servicePort: +# - serviceName: +# servicePort: # entryPoints: -# - +# - +replicated: + imagePullSecret: + enabled: false diff --git a/applications/wg-easy/charts/traefik/Chart.lock b/applications/wg-easy/charts/traefik/Chart.lock index aadcaee4..fecf2b0a 100644 --- a/applications/wg-easy/charts/traefik/Chart.lock +++ b/applications/wg-easy/charts/traefik/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: 28.0.0 - name: templates repository: file://../templates - version: 1.0.0 -digest: sha256:14c6de6f10918ec6bbe2d6e99408da62b362fc7950ce8793ebaaa4693ffdeb75 -generated: "2025-05-06T15:35:53.545992-04:00" + version: 1.1.0 +digest: sha256:4a28a4d4aff1811af81f160bee361f88262e2622a9df7fa36369cc1d44c72739 +generated: "2025-06-25T10:58:41.096107-04:00" diff --git a/applications/wg-easy/charts/traefik/values.yaml b/applications/wg-easy/charts/traefik/values.yaml index 94113ada..324d229b 100644 --- a/applications/wg-easy/charts/traefik/values.yaml +++ b/applications/wg-easy/charts/traefik/values.yaml @@ -5,7 +5,7 @@ certs: dnsNames: [] traefik: image: - registry: docker.io + registry: index.docker.io repository: traefik service: type: NodePort diff --git a/applications/wg-easy/charts/wg-easy/Chart.lock b/applications/wg-easy/charts/wg-easy/Chart.lock index b9b323fd..265e1306 100644 --- a/applications/wg-easy/charts/wg-easy/Chart.lock +++ b/applications/wg-easy/charts/wg-easy/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: 3.7.3 - name: templates repository: file://../templates - version: 1.0.0 -digest: sha256:4299a659fd462eb3faa8d3edd7930d66aad60bb19842777aa8a54e89e8aeee6f -generated: "2025-05-09T10:01:18.649929-04:00" + version: 1.1.0 +digest: sha256:b31a8b14ce1e7d0bb2452ff43d6e5433bd438c86cff3138c4a028902950e9884 +generated: "2025-06-25T10:58:36.514573-04:00" diff --git a/applications/wg-easy/helmfile.yaml.gotmpl b/applications/wg-easy/helmfile.yaml.gotmpl index e23269e5..df3e0c1a 100644 --- a/applications/wg-easy/helmfile.yaml.gotmpl +++ b/applications/wg-easy/helmfile.yaml.gotmpl @@ -19,18 +19,43 @@ environments: enableReplicatedSDK: false replicated: values: - - app: '{{ env "REPLICATED_APP" | default "wg-easy" }}' + - app: '{{ env "APP_SLUG" | default "wg-easy" }}' - channel: '{{ env "CHANNEL" | default "unstable" }}' - username: "test@example.com" - password: '{{env "REPLICATED_LICENSE_ID"}}' - chartSources: - certManager: 'oci://registry.replicated.com/{{ env "REPLICATED_APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/cert-manager' - certManagerIssuers: 'oci://registry.replicated.com/{{ env "REPLICATED_APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/cert-manager-issuers' - traefik: 'oci://registry.replicated.com/{{ env "REPLICATED_APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/traefik' - wgEasy: 'oci://registry.replicated.com/{{ env "REPLICATED_APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/wg-easy' - replicatedSDK: 'oci://registry.replicated.com/{{ env "REPLICATED_APP" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/replicated' + certManager: 'oci://registry.replicated.com/{{ env "APP_SLUG" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/cert-manager' + certManagerIssuers: 'oci://registry.replicated.com/{{ env "APP_SLUG" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/cert-manager-issuers' + traefik: 'oci://registry.replicated.com/{{ env "APP_SLUG" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/traefik' + wgEasy: 'oci://registry.replicated.com/{{ env "APP_SLUG" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/wg-easy' + replicatedSDK: 'oci://registry.replicated.com/{{ env "APP_SLUG" | default "wg-easy" }}/{{ env "CHANNEL" | default "unstable" }}/replicated' - extras: enableReplicatedSDK: true + # Replicated Registry Proxy configurations for container images + - proxyImages: + wgEasy: + image: + repository: proxy.replicated.com/proxy/wg-easy-cre/ghcr.io/wg-easy/wg-easy + traefik: + image: + registry: proxy.replicated.com/proxy/wg-easy-cre/index.docker.io + repository: library/traefik + certManager: + image: + registry: proxy.replicated.com/proxy/wg-easy-cre/quay.io + repository: jetstack/cert-manager-controller + webhook: + image: + registry: proxy.replicated.com/proxy/wg-easy-cre/quay.io + repository: jetstack/cert-manager-webhook + cainjector: + image: + registry: proxy.replicated.com/proxy/wg-easy-cre/quay.io + repository: jetstack/cert-manager-cainjector + startupapicheck: + image: + registry: proxy.replicated.com/proxy/wg-easy-cre/quay.io + repository: jetstack/cert-manager-startupapicheck --- {{- if eq .Environment.Name "replicated" }} repositories: @@ -51,6 +76,32 @@ releases: wait: true installed: true skipDeps: true +{{- if eq .Environment.Name "replicated" }} + values: + - templates: + replicated: + imagePullSecret: + enabled: true + - cert-manager: + image: + registry: {{ .Values.proxyImages.certManager.image.registry }} + repository: {{ .Values.proxyImages.certManager.image.repository }} + webhook: + image: + registry: {{ .Values.proxyImages.certManager.webhook.image.registry }} + repository: {{ .Values.proxyImages.certManager.webhook.image.repository }} + cainjector: + image: + registry: {{ .Values.proxyImages.certManager.cainjector.image.registry }} + repository: {{ .Values.proxyImages.certManager.cainjector.image.repository }} + startupapicheck: + image: + registry: {{ .Values.proxyImages.certManager.startupapicheck.image.registry }} + repository: {{ .Values.proxyImages.certManager.startupapicheck.image.repository }} + global: + imagePullSecrets: + - name: replicated-pull-secret +{{- end }} # Install issuers separately after cert-manager is ready - name: cert-manager-issuers @@ -63,6 +114,16 @@ releases: skipDeps: true needs: - cert-manager/cert-manager +{{- if eq .Environment.Name "replicated" }} + values: + - cert-manager: + image: + registry: {{ .Values.proxyImages.certManager.image.registry }} + repository: {{ .Values.proxyImages.certManager.image.repository }} + global: + imagePullSecrets: + - name: replicated-pull-secret +{{- end }} - name: traefik namespace: traefik @@ -81,6 +142,18 @@ releases: nodePort: 30080 websecure: nodePort: 30443 +{{- if eq .Environment.Name "replicated" }} + image: + registry: {{ .Values.proxyImages.traefik.image.registry }} + repository: {{ .Values.proxyImages.traefik.image.repository }} + deployment: + imagePullSecrets: + - name: replicated-pull-secret + - templates: + replicated: + imagePullSecret: + enabled: true +{{- end }} # Install replicated-sdk (only in replicated environment) - name: replicated @@ -93,7 +166,13 @@ releases: skipDeps: true needs: - traefik/traefik + values: + - templates: + replicated: + imagePullSecret: + enabled: true + # Install wg-easy - name: wg-easy namespace: wg-easy chart: {{ .Values.chartSources.wgEasy }} @@ -108,6 +187,21 @@ releases: - wg-easy: wireguard: host: '{{ env "TF_EXPOSED_URL" }}' +{{- if eq .Environment.Name "replicated" }} + controllers: + wg-easy: + containers: + wg-container: + image: + repository: {{ .Values.proxyImages.wgEasy.image.repository }} + pod: + imagePullSecrets: + - name: replicated-pull-secret + - templates: + replicated: + imagePullSecret: + enabled: true +{{- end }} - templates: traefikRoutes: web-tls: diff --git a/applications/wg-easy/task-dependency-graph.md b/applications/wg-easy/task-dependency-graph.md new file mode 100644 index 00000000..f858044f --- /dev/null +++ b/applications/wg-easy/task-dependency-graph.md @@ -0,0 +1,360 @@ +# WG-Easy Taskfile Dependency Graph + +## Visual Dependency Flow + +```mermaid +graph TD + %% Infrastructure Setup Chain + CC[cluster-create
📥 CLUSTER_NAME, K8S_VERSION
📤 cluster ready] --> SK[setup-kubeconfig
📥 CLUSTER_NAME
📤 KUBECONFIG_FILE] + SK --> CPE[cluster-ports-expose
📥 CLUSTER_NAME, EXPOSE_PORTS
📤 exposed URLs] + CPE --> HI[helm-install
📥 CLUSTER_NAME, HELM_ENV, CHANNEL
📤 deployed charts] + VK[verify-kubeconfig
📥 CLUSTER_NAME
📤 validated config] --> SK + + %% Chart Development Chain + HRA[helm-repo-add
📥 Chart.yaml files
📤 repo index] --> DU[dependencies-update
📥 Chart.yaml files
📤 updated deps] + DU --> CLA[chart-lint-all
📥 chart directories
📤 lint results] + DU --> CTA[chart-template-all
📥 chart directories
📤 template validation] + CLA --> CV[chart-validate
📥 chart directories
📤 validation status] + CTA --> CV + + %% Release Chain + DU --> RP[release-prepare
📥 chart directories
📤 release/ directory] + RP --> RC[release-create
📥 RELEASE_CHANNEL, RELEASE_VERSION
📤 release sequence] + + %% Channel Management (NEW) + CCH[channel-create
📥 RELEASE_CHANNEL
📤 CHANNEL_ID] --> CCR[customer-create
📥 CUSTOMER_NAME, RELEASE_CHANNEL_ID
📤 CUSTOMER_ID] + CCH --> RC + + %% Test Workflows + CC --> FTC[full-test-cycle
📥 CLUSTER_NAME
📤 test results] + SK --> FTC + CPE --> FTC + DU --> FTC + HI --> FTC + T[test
📥 running cluster
📤 test status] --> FTC + CD[cluster-delete
📥 CLUSTER_NAME
📤 cleanup status] --> FTC + + %% Customer Workflow (UPDATED) + CC --> CFTC[customer-full-test-cycle
📥 CUSTOMER_NAME, CLUSTER_NAME
📤 deployment status] + SK --> CFTC + CPE --> CFTC + CCR --> CFTC + CHI[customer-helm-install
📥 CUSTOMER_NAME, CLUSTER_NAME, CHANNEL_ID
📤 deployment status] --> CFTC + T --> CFTC + + %% PR Validation Workflow (UPDATED) + CV --> PVC[pr-validation-cycle
📥 BRANCH_NAME
📤 validation status] + CCH --> PVC + CCR --> PVC + CC --> PVC + SK --> PVC + CPE --> PVC + CHI --> PVC + T --> PVC + + %% Cleanup (UPDATED) + CD --> CPR[cleanup-pr-resources
📥 BRANCH_NAME
📤 cleanup status] + CUST_DEL[customer-delete
📥 CUSTOMER_ID
📤 archive status] --> CPR + CH_DEL[channel-delete
📥 RELEASE_CHANNEL_ID
📤 archive status] --> CPR + + %% Utility Dependencies + CC -.-> UWC[utils:wait-for-cluster
📥 CLUSTER_NAME, TIMEOUT
📤 ready status] + SK -.-> UGK[utils:get-kubeconfig
📥 CLUSTER_NAME
📤 kubeconfig file] + CPE -.-> UPO[utils:port-operations
📥 CLUSTER_NAME, OPERATION
📤 port status/URLs] + CCR -.-> UGL[utils:get-customer-license
📥 CUSTOMER_NAME
📤 LICENSE_ID] + + %% Container Workflows + DS[dev:start
📥 DEV_CONTAINER_*
📤 container ready] --> DSH[dev:shell
📥 container name
📤 shell session] + DS --> DR[dev:restart
📥 container config
📤 restarted container] + DST[dev:stop
📥 container name
📤 stopped container] --> DR + + %% Airgap Build (UPDATED) + AB[airgap-build
📥 RELEASE_CHANNEL/RELEASE_CHANNEL_ID
📤 airgap bundle status] +``` + +## Task Variable Reference + +### Infrastructure Tasks + +#### `cluster-create` + +- **Inputs**: `CLUSTER_NAME`, `K8S_VERSION`, `DISK_SIZE`, `INSTANCE_TYPE`, `DISTRIBUTION`, `EMBEDDED`, `TTL`, `REPLICATED_LICENSE_ID` (for embedded) +- **Outputs**: Cluster ready status, normalized cluster name +- **Dependencies**: None +- **Purpose**: Create test cluster using Replicated CMX + +#### `setup-kubeconfig` + +- **Inputs**: `CLUSTER_NAME`, `KUBECONFIG_FILE`, `DISTRIBUTION` +- **Outputs**: Kubeconfig file path +- **Dependencies**: `cluster-create`, `verify-kubeconfig` +- **Purpose**: Configure kubectl access and prepare cluster + +#### `cluster-ports-expose` + +- **Inputs**: `CLUSTER_NAME`, `EXPOSE_PORTS` +- **Outputs**: Exposed port URLs +- **Dependencies**: `cluster-create` +- **Purpose**: Expose cluster ports for external access + +#### `cluster-delete` +- **Inputs**: `CLUSTER_NAME` +- **Outputs**: Cleanup status +- **Dependencies**: None +- **Purpose**: Clean up test clusters and kubeconfig files + +### Chart Development Tasks + +#### `helm-repo-add` +- **Inputs**: Chart.yaml files from charts/ directory +- **Outputs**: Updated Helm repository index +- **Dependencies**: None +- **Purpose**: Add required Helm repositories from Chart.yaml files + +#### `dependencies-update` +- **Inputs**: Chart directories with Chart.yaml files +- **Outputs**: Updated chart dependencies in charts/*/charts/ +- **Dependencies**: `helm-repo-add` +- **Purpose**: Update all chart dependencies + +#### `chart-lint-all` +- **Inputs**: Chart directories +- **Outputs**: Lint validation results +- **Dependencies**: `dependencies-update` +- **Purpose**: Lint all Helm charts for syntax errors + +#### `chart-template-all` +- **Inputs**: Chart directories +- **Outputs**: Template validation results +- **Dependencies**: `dependencies-update` +- **Purpose**: Template charts to validate syntax + +#### `chart-validate` +- **Inputs**: Chart directories, helmfile template +- **Outputs**: Complete validation status +- **Dependencies**: `chart-lint-all`, `chart-template-all` +- **Purpose**: Complete chart validation including helmfile + +#### `chart-package-all` +- **Inputs**: Chart directories +- **Outputs**: Packaged .tgz files in release/ directory +- **Dependencies**: `dependencies-update`, `release-prepare` +- **Purpose**: Package charts for distribution + +### Channel Management Tasks (Enhanced) + +#### `channel-create` +- **Inputs**: `RELEASE_CHANNEL`, `APP_SLUG` +- **Outputs**: `CHANNEL_ID` (unique identifier) +- **Dependencies**: None +- **Purpose**: Create release channel and return unique ID + +#### `channel-delete` +- **Inputs**: `RELEASE_CHANNEL_ID`, `APP_SLUG` +- **Outputs**: Archive status +- **Dependencies**: None +- **Purpose**: Archive release channel by unique ID + +### Customer Management Tasks (Updated) + +#### `customer-create` +- **Inputs**: `CUSTOMER_NAME`, `CUSTOMER_EMAIL`, `RELEASE_CHANNEL`/`RELEASE_CHANNEL_ID`, `LICENSE_TYPE`, `EXPIRES_IN`, `APP_SLUG` +- **Outputs**: `CUSTOMER_ID` +- **Dependencies**: None +- **Purpose**: Create customer and return unique ID + +#### `customer-delete` +- **Inputs**: `CUSTOMER_ID`, `APP_SLUG` +- **Outputs**: Archive status +- **Dependencies**: None +- **Purpose**: Archive customer by unique ID + +### Deployment Tasks (Updated) + +#### `helm-install` +- **Inputs**: `CLUSTER_NAME`, `HELM_ENV`, `REPLICATED_LICENSE_ID`, `CHANNEL` (ID or slug), `KUBECONFIG_FILE` +- **Outputs**: Deployment status +- **Dependencies**: `setup-kubeconfig`, `cluster-ports-expose` +- **Purpose**: Deploy charts using helmfile + +#### `customer-helm-install` +- **Inputs**: `CUSTOMER_NAME`, `CLUSTER_NAME`, `REPLICATED_LICENSE_ID`, `CHANNEL_ID`/`CHANNEL_SLUG`, `KUBECONFIG_FILE` +- **Outputs**: Deployment status with customer registry authentication +- **Dependencies**: `setup-kubeconfig`, `cluster-ports-expose` +- **Purpose**: Deploy using customer license and registry authentication + +### Release Tasks + +#### `release-prepare` +- **Inputs**: Chart directories, replicated YAML files +- **Outputs**: release/ directory with prepared artifacts +- **Dependencies**: `dependencies-update` +- **Purpose**: Prepare release artifacts including packaged charts + +#### `release-create` +- **Inputs**: `RELEASE_CHANNEL`, `RELEASE_VERSION`, `RELEASE_NOTES`, `APP_SLUG` +- **Outputs**: Release sequence number +- **Dependencies**: `release-prepare` +- **Purpose**: Create and promote Replicated release + +### Workflow Orchestrators (Updated) + +#### `full-test-cycle` +- **Inputs**: `CLUSTER_NAME` and all chart/deployment parameters +- **Outputs**: Complete test cycle status +- **Dependencies**: 8 tasks (create→setup→expose→update→preflight→install→test→delete) +- **Purpose**: Complete testing workflow with cleanup + +#### `customer-full-test-cycle` +- **Inputs**: `CUSTOMER_NAME`, `CLUSTER_NAME` +- **Outputs**: Customer deployment status +- **Dependencies**: 7 tasks (create→setup→expose→customer-create→customer-install→test) +- **Purpose**: Customer-focused testing workflow (no cleanup for CD) + +#### `pr-validation-cycle` (Enhanced) +- **Inputs**: `BRANCH_NAME`, `CHANNEL_NAME` +- **Outputs**: Complete PR validation status, `CHANNEL_ID` +- **Dependencies**: 9 tasks (validate→channel-create→release→customer-create→cluster-create→setup→expose→deploy→test) +- **Purpose**: Complete PR validation workflow with channel ID management + +#### `cleanup-pr-resources` (Updated) +- **Inputs**: `BRANCH_NAME`, `CHANNEL_NAME` +- **Outputs**: Cleanup status +- **Dependencies**: 3 cleanup tasks (cluster-delete, customer-delete, channel-delete) +- **Purpose**: Clean up PR test resources using proper ID lookups + +### Utility Tasks (Enhanced) + +#### `utils:get-customer-license` +- **Inputs**: `CUSTOMER_NAME` (normalized) +- **Outputs**: `REPLICATED_LICENSE_ID` +- **Dependencies**: None +- **Purpose**: Retrieve customer license ID by normalized name + +#### `utils:port-operations` +- **Inputs**: `CLUSTER_NAME`, `OPERATION` (expose/getenv), `EXPOSE_PORTS` +- **Outputs**: Port status or environment variables (TF_EXPOSED_URL) +- **Dependencies**: None +- **Purpose**: Manage cluster port exposure and URL retrieval + +#### `utils:wait-for-cluster` +- **Inputs**: `CLUSTER_NAME`, `TIMEOUT` +- **Outputs**: Cluster ready status +- **Dependencies**: None +- **Purpose**: Wait for cluster to reach running state + +### Airgap Tasks (Updated) + +#### `airgap-build` +- **Inputs**: `RELEASE_CHANNEL`/`RELEASE_CHANNEL_ID`, `APP_SLUG` +- **Outputs**: Airgap bundle build status +- **Dependencies**: None +- **Purpose**: Build airgap bundle for releases, supports both channel names and IDs + +## Task Complexity Levels + +### Simple Tasks (No Dependencies) +- `default`, `test`, `cluster-list` +- `customer-create`, `customer-ls`, `customer-delete` +- `channel-create`, `channel-delete` (Enhanced with ID support) +- `clean`, `airgap-build` +- All `dev:*` base tasks +- All `utils:*` tasks + +### Moderate Tasks (1-2 Dependencies) +- `dependencies-update` → `helm-repo-add` +- `chart-lint-all` → `dependencies-update` +- `chart-template-all` → `dependencies-update` +- `setup-kubeconfig` → `cluster-create`, `verify-kubeconfig` +- `cluster-ports-expose` → `cluster-create` + +### Complex Tasks (3+ Dependencies) +- `helm-install` → `setup-kubeconfig`, `cluster-ports-expose` +- `chart-validate` → `chart-lint-all`, `chart-template-all` +- `release-create` → `release-prepare` → `dependencies-update` + +### Workflow Orchestrators (High Complexity) +- **full-test-cycle**: 8 task calls +- **customer-full-test-cycle**: 7 task calls +- **pr-validation-cycle**: 9 task calls (Enhanced with channel ID flow) +- **cleanup-pr-resources**: 3 cleanup task calls (Enhanced with ID lookups) + +## Critical Path Analysis + +### For Development (Chart Testing) + +```text +helm-repo-add → dependencies-update → chart-lint-all/chart-template-all → chart-validate +``` + +### For Deployment Testing + +```text +cluster-create → setup-kubeconfig → cluster-ports-expose → helm-install → test +``` + +### For Release Management (Enhanced) + +```text +helm-repo-add → dependencies-update → release-prepare → channel-create → release-create +📤 CHANNEL_ID for downstream usage +``` + +### For PR Validation (Enhanced Flow) + +```text +chart-validate → channel-create → release-create → customer-create → cluster-create → +setup-kubeconfig → cluster-ports-expose → customer-helm-install → test +📤 CHANNEL_ID flows through customer-create and customer-helm-install +``` + +### For Customer Workflows (Enhanced) + +```text +customer-create (with CHANNEL_ID) → cluster-create → setup-kubeconfig → +cluster-ports-expose → customer-helm-install (with CHANNEL_ID) → test +``` + +## Channel ID Enhancement Benefits + +### Unique Identification +- **Channel IDs**: Eliminate ambiguity with duplicate channel names across apps +- **Precise Targeting**: Tasks use unique identifiers for reliable channel operations +- **Error Reduction**: Reduced chance of operating on wrong channels + +### Improved Data Flow +- **ID Propagation**: Channel IDs flow from creation through deployment +- **Backward Compatibility**: Tasks accept both channel names and IDs +- **Flexible Usage**: Supports both automated workflows and manual operations + +### Enhanced Workflows +- **GitHub Actions**: Pass precise channel IDs between workflow jobs +- **Customer Management**: Create customers with specific channel IDs +- **Deployment Targeting**: Deploy to exact channels using IDs + +## Variable Naming Conventions + +### Input Variables +- `*_NAME`: Human-readable names (normalized for slugs) +- `*_ID`: Unique identifiers from Replicated API +- `*_SLUG`: URL-safe identifiers (legacy, prefer IDs) +- `NORMALIZED_*`: Transformed names for API compatibility + +### Output Variables +- Functions return primary identifiers (IDs where available) +- Status outputs indicate success/failure +- File paths for generated artifacts + +### Environment Variables +- `APP_SLUG`: Application identifier +- `REPLICATED_*`: API tokens and app references +- `KUBECONFIG`: Cluster access configuration + +## Dependency Characteristics + +- **Linear Dependencies**: Most tasks follow clear sequential patterns +- **Parallel Opportunities**: Chart validation tasks can run in parallel +- **Resource Dependencies**: Infrastructure tasks must run in order +- **Cleanup Isolation**: Cleanup tasks are independent of build/deploy chains +- **Utility Abstraction**: Common operations abstracted to utils namespace +- **ID Management**: Channel and customer IDs provide reliable resource targeting diff --git a/applications/wg-easy/taskfiles/utils.yml b/applications/wg-easy/taskfiles/utils.yml index 0b216663..972071d9 100644 --- a/applications/wg-easy/taskfiles/utils.yml +++ b/applications/wg-easy/taskfiles/utils.yml @@ -1,10 +1,114 @@ version: "3" tasks: + install-replicated-cli: + desc: Install the latest Replicated CLI binary + silent: false + run: once + status: + - command -v replicated >/dev/null 2>&1 + cmds: + - | + echo "Installing Replicated CLI..." + + # Detect OS and architecture + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + # Map architecture names + case $ARCH in + x86_64) + ARCH="amd64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; + esac + + echo "Detected OS: $OS, Architecture: $ARCH" + + # Create a temporary directory for extraction + TEMP_DIR=$(mktemp -d) + cd "$TEMP_DIR" + + # Download and install based on OS + if [ "$OS" = "linux" ]; then + echo "Downloading Replicated CLI for Linux..." + echo "DEBUG: Looking for pattern: _linux_${ARCH}.tar.gz" + + # Try API call with error handling + API_RESPONSE=$(curl -s https://api.github.com/repos/replicatedhq/replicated/releases/latest 2>/dev/null || echo "null") + + if [ "$API_RESPONSE" = "null" ] || [ -z "$API_RESPONSE" ]; then + echo "GitHub API unavailable, using direct download URL..." + DOWNLOAD_URL="https://github.com/replicatedhq/replicated/releases/latest/download/replicated_linux_${ARCH}.tar.gz" + else + DOWNLOAD_URL=$(echo "$API_RESPONSE" | grep "browser_download_url.*_linux_${ARCH}.tar.gz" | cut -d '"' -f 4) + fi + + echo "DEBUG: Found download URL: $DOWNLOAD_URL" + + if [ -z "$DOWNLOAD_URL" ]; then + echo "Error: Could not find download URL for linux_${ARCH}.tar.gz" + echo "DEBUG: Trying fallback URL pattern..." + DOWNLOAD_URL="https://github.com/replicatedhq/replicated/releases/latest/download/replicated_linux_${ARCH}.tar.gz" + fi + + curl -L -o replicated.tar.gz "$DOWNLOAD_URL" + tar xzf replicated.tar.gz + sudo mv replicated /usr/local/bin/replicated + + elif [ "$OS" = "darwin" ]; then + echo "Downloading Replicated CLI for macOS..." + echo "DEBUG: Looking for pattern: _darwin_${ARCH}.tar.gz" + DOWNLOAD_URL=$(curl -s https://api.github.com/repos/replicatedhq/replicated/releases/latest \ + | grep "browser_download_url.*_darwin_${ARCH}.tar.gz" \ + | cut -d '"' -f 4) + + echo "DEBUG: Found download URL: $DOWNLOAD_URL" + + if [ -z "$DOWNLOAD_URL" ]; then + echo "Error: Could not find download URL for darwin_${ARCH}.tar.gz" + echo "DEBUG: Available assets:" + curl -s https://api.github.com/repos/replicatedhq/replicated/releases/latest | jq -r '.assets[].name' + exit 1 + fi + + curl -L -o replicated.tar.gz "$DOWNLOAD_URL" + tar xzf replicated.tar.gz + sudo mv replicated /usr/local/bin/replicated + + else + echo "Unsupported operating system: $OS" + echo "Please install manually from: https://docs.replicated.com/reference/replicated-cli-installing" + cd - >/dev/null + rm -rf "$TEMP_DIR" + exit 1 + fi + + # Clean up temporary directory + cd - >/dev/null + rm -rf "$TEMP_DIR" + + # Verify installation + if command -v replicated >/dev/null 2>&1; then + echo "Replicated CLI installed successfully!" + replicated version + else + echo "Failed to install Replicated CLI" + exit 1 + fi get-kubeconfig: desc: Get kubeconfig for the test cluster (internal) internal: true run: once + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE}}' cmds: - | echo "Getting kubeconfig for cluster {{.CLUSTER_NAME}}..." @@ -16,6 +120,9 @@ tasks: desc: Remove pre-installed Traefik from k3s clusters (internal) internal: true run: once + vars: + CLUSTER_NAME: '{{.CLUSTER_NAME}}' + KUBECONFIG_FILE: '{{.KUBECONFIG_FILE}}' status: - | # Only check if we need to run this for k3s distributions @@ -152,6 +259,38 @@ tasks: exit 1 fi + get-customer-license: + desc: Retrieve a customer's license ID by name + silent: false + vars: + CUSTOMER_NAME: '{{.CUSTOMER_NAME | default ""}}' + # Normalize customer name by replacing common git branch delimiters with hyphens + # This matches how customer slugs are represented in the Replicated Vendor Portal backend + NORMALIZED_CUSTOMER_NAME: + sh: task utils:normalize-name NAME="{{.CUSTOMER_NAME}}" + cmds: + - | + if [ -z "{{.CUSTOMER_NAME}}" ]; then + echo "ERROR: CUSTOMER_NAME is required" + echo "Usage: task utils:get-customer-license CUSTOMER_NAME=your-customer-name" + exit 1 + fi + + echo "Looking up license ID for customer: {{.NORMALIZED_CUSTOMER_NAME}}" + + # Get customer license ID using Replicated CLI + LICENSE_ID=$(replicated customer ls --output json | jq -r '.[] | select(.name == "{{.NORMALIZED_CUSTOMER_NAME}}") | .installationId') + + if [ -z "$LICENSE_ID" ] || [ "$LICENSE_ID" = "null" ]; then + echo "ERROR: Could not find customer with name '{{.NORMALIZED_CUSTOMER_NAME}}'" + echo "Available customers:" + replicated customer ls --output json | jq -r '.[] | " - \(.name) (ID: \(.id))"' + exit 1 + fi + + echo "Customer '{{.NORMALIZED_CUSTOMER_NAME}}' license ID: $LICENSE_ID" + echo "$LICENSE_ID" + gcp-operations: desc: GCP VM operations internal: true @@ -265,3 +404,15 @@ tasks: 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 + + normalize-name: + desc: Normalize a name by replacing git branch delimiters with hyphens + vars: + NAME: '{{.NAME}}' + cmds: + - | + if [ -z "{{.NAME}}" ]; then + echo "" + exit 0 + fi + echo "{{.NAME}}" | tr '/' '-' | tr '_' '-' | tr '.' '-'