mirror of https://github.com/fluxcd/flux2.git
				
				
				
			Merge branch 'main' into patch-3
						commit
						c9e9b1b8a6
					
				| @ -0,0 +1,16 @@ | ||||
| version: 2 | ||||
| 
 | ||||
| updates: | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     labels: ["area/ci", "dependencies"] | ||||
|     groups: | ||||
|       # Group all updates together, so that they are all applied in a single PR. | ||||
|       # Grouped updates are currently in beta and is subject to change. | ||||
|       # xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups | ||||
|       ci: | ||||
|         patterns: | ||||
|           - "*" | ||||
|     schedule: | ||||
|       # By default, this will be on a monday. | ||||
|       interval: "weekly" | ||||
| @ -0,0 +1,55 @@ | ||||
| # Configuration file to declaratively configure labels | ||||
| # Ref: https://github.com/EndBug/label-sync#Config-files | ||||
| 
 | ||||
| - name: area/bootstrap | ||||
|   description: Bootstrap related issues and pull requests | ||||
|   color: '#86efc9' | ||||
| - name: area/install | ||||
|   description: Install and uninstall related issues and pull requests | ||||
|   color: '#86efc9' | ||||
| - name: area/diff | ||||
|   description: Diff related issues and pull requests | ||||
|   color: '#BA4192' | ||||
| - name: area/bucket | ||||
|   description: Bucket related issues and pull requests | ||||
|   color: '#00b140' | ||||
| - name: area/git | ||||
|   description: Git related issues and pull requests | ||||
|   color: '#863faf' | ||||
| - name: area/oci | ||||
|   description: OCI related issues and pull requests | ||||
|   color: '#c739ff' | ||||
| - name: area/kustomization | ||||
|   description: Kustomization related issues and pull requests | ||||
|   color: '#00e54d' | ||||
| - name: area/helm | ||||
|   description: Helm related issues and pull requests | ||||
|   color: '#1673b6' | ||||
| - name: area/image-automation | ||||
|   description: Automated image updates related issues and pull requests | ||||
|   color: '#c5def5' | ||||
| - name: area/monitoring | ||||
|   description: Monitoring related issues and pull requests | ||||
|   color: '#dd75ae' | ||||
| - name: area/multi-tenancy | ||||
|   description: Multi-tenancy related issues and pull requests | ||||
|   color: '#72CDBD' | ||||
| - name: area/notification | ||||
|   description: Notification API related issues and pull requests | ||||
|   color: '#434ec1' | ||||
| - name: area/source | ||||
|   description: Source API related issues and pull requests | ||||
|   color: '#863faf' | ||||
| - name: area/rfc | ||||
|   description: Feature request proposals in the RFC format | ||||
|   color: '#D621C3' | ||||
|   aliases: ['area/RFC'] | ||||
| - name: backport:release/v2.0.x | ||||
|   description: To be backported to release/v2.0.x | ||||
|   color: '#ffd700' | ||||
| - name: backport:release/v2.1.x | ||||
|   description: To be backported to release/v2.1.x | ||||
|   color: '#ffd700' | ||||
| - name: backport:release/v2.2.x | ||||
|   description: To be backported to release/v2.2.x | ||||
|   color: '#ffd700' | ||||
| @ -1,42 +1,80 @@ | ||||
| # Flux GitHub runners | ||||
| # Flux ARM64 GitHub runners | ||||
| 
 | ||||
| How to provision GitHub Actions self-hosted runners for Flux conformance testing. | ||||
| The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners. | ||||
| 
 | ||||
| ## ARM64 Instance specs | ||||
| ## Current instances | ||||
| 
 | ||||
| In order to add a new runner to the GitHub Actions pool, | ||||
| first create an instance on Oracle Cloud with the following configuration: | ||||
| - OS: Canonical Ubuntu 20.04 | ||||
| - Shape: VM.Standard.A1.Flex | ||||
| - OCPU Count: 2  | ||||
| - Memory (GB): 12 | ||||
| - Network Bandwidth (Gbps): 2 | ||||
| - Local Disk: Block Storage Only   | ||||
| | Repository                  | Runner           | Instance               | Location      | | ||||
| |-----------------------------|------------------|------------------------|---------------| | ||||
| | flux2                       | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | | ||||
| | flux2                       | equinix-arm-dc-2 | flux-equinix-arm-dc-01 | Washington DC | | ||||
| | flux2                       | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas        | | ||||
| | flux2                       | equinix-arm-da-2 | flux-equinix-arm-da-01 | Dallas        | | ||||
| | source-controller           | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | | ||||
| | source-controller           | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas        | | ||||
| | image-automation-controller | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | | ||||
| | image-automation-controller | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas        | | ||||
| 
 | ||||
| Instance spec: | ||||
| - Ampere Altra Q80-30 80-core processor @ 2.8GHz | ||||
| - 2 x 960GB NVME | ||||
| - 256GB RAM | ||||
| - 2 x 25Gbps | ||||
| 
 | ||||
| Note that the instance image source must be **Canonical Ubuntu** instead of the default Oracle Linux. | ||||
| ## Instance setup | ||||
| 
 | ||||
| ## ARM64 Instance setup | ||||
| In order to add a new runner to the GitHub Actions pool, | ||||
| first create a server on Equinix with the following configuration: | ||||
| - Type: `c3.large.arm64` | ||||
| - OS: `Ubuntu 22.04 LTS` | ||||
| 
 | ||||
| ### Install prerequisites | ||||
| 
 | ||||
| - SSH into a newly created instance | ||||
| ```shell | ||||
| ssh ubuntu@<instance-public-IP> | ||||
| ssh root@<instance-public-IP> | ||||
| ```  | ||||
| - Create the action runner dir | ||||
| 
 | ||||
| - Create the ubuntu user | ||||
| ```shell | ||||
| adduser ubuntu | ||||
| usermod -aG sudo ubuntu | ||||
| su - ubuntu | ||||
| ``` | ||||
| 
 | ||||
| - Create the prerequisites dir | ||||
| ```shell | ||||
| mkdir -p prereq && cd prereq | ||||
| ``` | ||||
| 
 | ||||
| - Download the prerequisites script | ||||
| ```shell | ||||
| mkdir -p actions-runner && cd actions-runner | ||||
| curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/prereq.sh > prereq.sh \ | ||||
|   && chmod +x ./prereq.sh | ||||
| ``` | ||||
| - Download the provisioning script | ||||
| 
 | ||||
| - Install the prerequisites | ||||
| ```shell | ||||
| curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/arm64.sh > arm64.sh \ | ||||
|   && chmod +x ./arm64.sh | ||||
| sudo ./prereq.sh | ||||
| ``` | ||||
| 
 | ||||
| ### Install runners | ||||
| 
 | ||||
| - Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux) | ||||
| - Run the provisioning script passing the token as the first argument | ||||
| 
 | ||||
| - Create two directories `flux2-01`, `flux2-02` | ||||
| 
 | ||||
| - In each dir run: | ||||
| ```shell | ||||
| sudo ./arm64.sh <TOKEN> | ||||
| curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/runner-setup.sh > runner-setup.sh \ | ||||
|   && chmod +x ./runner-setup.sh | ||||
| 
 | ||||
| ./runner-setup.sh equinix-arm-<NUMBER> <TOKEN> <REPO> | ||||
| ``` | ||||
| 
 | ||||
| - Reboot the instance | ||||
| ```shell | ||||
| sudo reboot | ||||
| ```   | ||||
| ``` | ||||
| 
 | ||||
| - Navigate to the GitHub repository [runners page](https://github.com/fluxcd/flux2/settings/actions/runners) and check the runner status | ||||
|  | ||||
| @ -0,0 +1,37 @@ | ||||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| # Copyright 2021 The Flux authors. All rights reserved. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| 
 | ||||
| # This script installs a GitHub self-hosted ARM64 runner for running Flux end-to-end tests. | ||||
| 
 | ||||
| set -eu | ||||
| 
 | ||||
| RUNNER_NAME=$1 | ||||
| REPOSITORY_TOKEN=$2 | ||||
| REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2} | ||||
| 
 | ||||
| GITHUB_RUNNER_VERSION=2.298.2 | ||||
| 
 | ||||
| # download runner | ||||
| curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \ | ||||
|   && tar xzf actions-runner-linux-arm64.tar.gz \ | ||||
|   && rm actions-runner-linux-arm64.tar.gz | ||||
| 
 | ||||
| # register runner with GitHub | ||||
| ./config.sh --unattended --url ${REPOSITORY_URL} --token ${REPOSITORY_TOKEN} --name ${RUNNER_NAME} | ||||
| 
 | ||||
| # start runner | ||||
| sudo ./svc.sh install | ||||
| sudo ./svc.sh start | ||||
| @ -0,0 +1,50 @@ | ||||
| # Flux GitHub Workflows | ||||
| 
 | ||||
| ## End-to-end Testing | ||||
| 
 | ||||
| The e2e workflows run a series of tests to ensure that the Flux CLI and | ||||
| the GitOps Toolkit controllers work well all together. | ||||
| The tests are written in Go, Bash, Make and Terraform. | ||||
| 
 | ||||
| | Workflow           | Jobs                 | Runner         | Role                                          | | ||||
| |--------------------|----------------------|----------------|-----------------------------------------------| | ||||
| | e2e.yaml           | e2e-amd64-kubernetes | GitHub Ubuntu  | integration testing with Kubernetes Kind<br/> | | ||||
| | e2e-arm64.yaml     | e2e-arm64-kubernetes | Equinix Ubuntu | integration testing with Kubernetes Kind<br/> | | ||||
| | e2e-bootstrap.yaml | e2e-boostrap-github  | GitHub Ubuntu  | integration testing with GitHub API<br/>      | | ||||
| | e2e-azure.yaml     | e2e-amd64-aks        | GitHub Ubuntu  | integration testing with Azure API<br/>       | | ||||
| | scan.yaml          | scan-fossa           | GitHub Ubuntu  | license scanning<br/>                         | | ||||
| | scan.yaml          | scan-snyk            | GitHub Ubuntu  | vulnerability scanning<br/>                   | | ||||
| | scan.yaml          | scan-codeql          | GitHub Ubuntu  | vulnerability scanning<br/>                   | | ||||
| 
 | ||||
| ## Components Update | ||||
| 
 | ||||
| The components update workflow scans the GitOps Toolkit controller repositories for new releases, | ||||
| amd when it finds a new controller version, the workflow performs the following steps: | ||||
| - Updates the controller API package version in `go.mod`. | ||||
| - Patches the controller CRDs version in the `manifests/crds` overlay. | ||||
| - Patches the controller Deployment version in `manifests/bases` overlay. | ||||
| - Opens a Pull Request against the `main` branch. | ||||
| - Triggers the e2e test suite to run for the opened PR. | ||||
| 
 | ||||
| 
 | ||||
| | Workflow    | Jobs              | Runner        | Role                                                | | ||||
| |-------------|-------------------|---------------|-----------------------------------------------------| | ||||
| | update.yaml | update-components | GitHub Ubuntu | update the GitOps Toolkit APIs and controllers<br/> | | ||||
| 
 | ||||
| ## Release | ||||
| 
 | ||||
| The release workflow is triggered by a semver Git tag and performs the following steps: | ||||
| - Generates the Flux install manifests (YAML). | ||||
| - Generates the OpenAPI validation schemas for the GitOps Toolkit CRDs (JSON). | ||||
| - Generates a Software Bill of Materials (SPDX JSON). | ||||
| - Builds the Flux CLI binaries and the multi-arch container images. | ||||
| - Pushes the container images to GitHub Container Registry and DockerHub. | ||||
| - Signs the sbom, the binaries checksum and the container images with Cosign and GitHub OIDC. | ||||
| - Uploads the sbom, binaries, checksums and install manifests to GitHub Releases. | ||||
| - Pushes the install manifests as OCI artifacts to GitHub Container Registry and DockerHub. | ||||
| - Signs the OCI artifacts with Cosign and GitHub OIDC. | ||||
| 
 | ||||
| | Workflow     | Jobs                   | Runner        | Role                                                 | | ||||
| |--------------|------------------------|---------------|------------------------------------------------------| | ||||
| | release.yaml | release-flux-cli       | GitHub Ubuntu | build, push and sign the CLI release artifacts<br/>  | | ||||
| | release.yaml | release-flux-manifests | GitHub Ubuntu | build, push and sign the Flux install manifests<br/> | | ||||
| @ -0,0 +1,29 @@ | ||||
| name: test-gh-action | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'action/**' | ||||
|   push: | ||||
|     paths: | ||||
|       - 'action/**' | ||||
|     branches: | ||||
|       - 'main' | ||||
|       - 'release/**' | ||||
| 
 | ||||
| permissions: read-all | ||||
| 
 | ||||
| jobs: | ||||
|   actions: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         version: [ubuntu-latest, macos-latest, windows-latest] | ||||
| 
 | ||||
|     runs-on: ${{ matrix.version }} | ||||
|     name: action on ${{ matrix.version }} | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Setup flux | ||||
|         uses: ./action | ||||
| @ -0,0 +1,31 @@ | ||||
| name: backport | ||||
| 
 | ||||
| on: | ||||
|   pull_request_target: | ||||
|     types: [closed, labeled] | ||||
| 
 | ||||
| jobs: | ||||
|   pull-request: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: write | ||||
|       pull-requests: write | ||||
|     if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name)) | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|         with: | ||||
|           ref: ${{ github.event.pull_request.head.sha }} | ||||
|       - name: Create backport PRs | ||||
|         uses: korthout/backport-action@addffea45a2f0b5682f1d5ba0506f45bc18bf174 # v2.3.0 | ||||
|         # xref: https://github.com/korthout/backport-action#inputs | ||||
|         with: | ||||
|           # Use token to allow workflows to be triggered for the created PR | ||||
|           github_token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           # Match labels with a pattern `backport:<target-branch>` | ||||
|           label_pattern: '^backport:([^ ]+)$' | ||||
|           # A bit shorter pull-request title than the default | ||||
|           pull_title: '[${target_branch}] ${pull_title}' | ||||
|           # Simpler PR description than default | ||||
|           pull_description: |- | ||||
|             Automated backport to `${target_branch}`, triggered by a label in #${pull_number}. | ||||
| @ -0,0 +1,102 @@ | ||||
| name: e2e-gcp | ||||
| 
 | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   schedule: | ||||
|     - cron: '0 6 * * *' | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - 'tests/**' | ||||
|       - '.github/workflows/e2e-gcp.yaml' | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - 'tests/**' | ||||
|       - '.github/workflows/e2e-gcp.yaml' | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   e2e-gcp: | ||||
|     runs-on: ubuntu-22.04 | ||||
|     defaults: | ||||
|       run: | ||||
|         working-directory: ./tests/integration | ||||
|     if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 | ||||
|         with: | ||||
|           go-version: 1.20.x | ||||
|           cache-dependency-path: tests/integration/go.sum | ||||
|       - name: Setup Flux CLI | ||||
|         run: make build | ||||
|         working-directory: ./ | ||||
|       - name: Setup SOPS | ||||
|         run: | | ||||
|           mkdir -p $HOME/.local/bin | ||||
|           wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux | ||||
|           chmod +x $HOME/.local/bin/sops | ||||
|         env: | ||||
|           SOPS_VER: 3.7.1 | ||||
|       - name: Authenticate to Google Cloud | ||||
|         uses: google-github-actions/auth@f6de81663f7788d05bd15bcce18f0e57f23f0846 # v2.0.1 | ||||
|         id: 'auth' | ||||
|         with: | ||||
|           credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}' | ||||
|           token_format: 'access_token' | ||||
|       - name: Setup gcloud | ||||
|         uses: google-github-actions/setup-gcloud@5a5f7b85fca43e76e53463acaa9d408a03c98d3a # v2.0.1 | ||||
|       - name: Setup QEMU | ||||
|         uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 | ||||
|       - name: Setup Docker Buildx | ||||
|         uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 | ||||
|       - name: Log into us-central1-docker.pkg.dev | ||||
|         uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 | ||||
|         with: | ||||
|           registry: us-central1-docker.pkg.dev | ||||
|           username: oauth2accesstoken | ||||
|           password: ${{ steps.auth.outputs.access_token }} | ||||
|       - name: Set dynamic variables in .env | ||||
|         run: | | ||||
|           cat > .env <<EOF | ||||
|           export TF_VAR_tags='{ "environment"="github", "ci"="true", "repo"="flux2", "createdat"="$(date -u +x%Y-%m-%d_%Hh%Mm%Ss)" }' | ||||
|           EOF | ||||
|       - name: Print .env for dynamic tag value reference | ||||
|         run: cat .env | ||||
|       - name: Run GCP e2e tests | ||||
|         env: | ||||
|           TF_VAR_gcp_project_id: ${{ vars.TF_VAR_gcp_project_id }} | ||||
|           TF_VAR_gcp_region: ${{ vars.TF_VAR_gcp_region }} | ||||
|           TF_VAR_gcp_zone: ${{ vars.TF_VAR_gcp_zone }} | ||||
|           TF_VAR_gcp_email: ${{ secrets.TF_VAR_gcp_email }} | ||||
|           TF_VAR_gcp_keyring: ${{ secrets.TF_VAR_gcp_keyring }} | ||||
|           TF_VAR_gcp_crypto_key: ${{ secrets.TF_VAR_gcp_crypto_key }} | ||||
|           GITREPO_SSH_CONTENTS: ${{ secrets.GCP_GITREPO_SSH_CONTENTS }} | ||||
|           GITREPO_SSH_PUB_CONTENTS: ${{ secrets.GCP_GITREPO_SSH_PUB_CONTENTS }} | ||||
|         run: | | ||||
|           source .env | ||||
|           mkdir -p ./build/ssh | ||||
|           touch ./build/ssh/key | ||||
|           echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key | ||||
|           export GITREPO_SSH_PATH=build/ssh/key | ||||
|           touch ./build/ssh/key.pub | ||||
|           echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub | ||||
|           export GITREPO_SSH_PUB_PATH=build/ssh/key.pub | ||||
|           make test-gcp | ||||
|       - name: Ensure resource cleanup | ||||
|         if: ${{ always() }} | ||||
|         env: | ||||
|           TF_VAR_gcp_project_id: ${{ vars.TF_VAR_gcp_project_id }} | ||||
|           TF_VAR_gcp_region: ${{ vars.TF_VAR_gcp_region }} | ||||
|           TF_VAR_gcp_zone: ${{ vars.TF_VAR_gcp_zone }} | ||||
|           TF_VAR_gcp_email: ${{ secrets.TF_VAR_gcp_email }} | ||||
|           TF_VAR_gcp_keyring: ${{ secrets.TF_VAR_gcp_keyring }} | ||||
|           TF_VAR_gcp_crypto_key: ${{ secrets.TF_VAR_gcp_crypto_key }} | ||||
|         run: source .env && make destroy-gcp | ||||
| @ -0,0 +1,39 @@ | ||||
| name: ossf | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   schedule: | ||||
|     # Weekly on Saturdays. | ||||
|     - cron:  '30 1 * * 6' | ||||
| 
 | ||||
| permissions: read-all | ||||
| 
 | ||||
| jobs: | ||||
|   scorecard: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       security-events: write | ||||
|       id-token: write | ||||
|       actions: read | ||||
|       contents: read | ||||
|     steps: | ||||
|       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Run analysis | ||||
|         uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 | ||||
|         with: | ||||
|           results_file: results.sarif | ||||
|           results_format: sarif | ||||
|           repo_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           publish_results: true | ||||
|       - name: Upload artifact | ||||
|         uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0 | ||||
|         with: | ||||
|           name: SARIF file | ||||
|           path: results.sarif | ||||
|           retention-days: 5 | ||||
|       - name: Upload SARIF results | ||||
|         uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 | ||||
|         with: | ||||
|           sarif_file: results.sarif | ||||
| @ -1,21 +0,0 @@ | ||||
| name: rebase | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [ opened ] | ||||
|   issue_comment: | ||||
|     types: [ created ] | ||||
| 
 | ||||
| jobs: | ||||
|   rebase: | ||||
|     if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the latest code | ||||
|         uses: actions/checkout@v2 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|       - name: Automatic Rebase | ||||
|         uses: cirrus-actions/rebase@1.3.1 | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
| @ -1,60 +1,85 @@ | ||||
| name: Scan | ||||
| name: scan | ||||
| 
 | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: [ main ] | ||||
|     branches: [ 'main', 'release/**' ] | ||||
|   pull_request: | ||||
|     branches: [ main ] | ||||
|     branches: [ 'main', 'release/**' ] | ||||
|   schedule: | ||||
|     - cron: '18 10 * * 3' | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   fossa: | ||||
|     name: FOSSA | ||||
|   scan-fossa: | ||||
|     runs-on: ubuntu-latest | ||||
|     if: github.actor != 'dependabot[bot]' | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Run FOSSA scan and upload build data | ||||
|         uses: fossa-contrib/fossa-action@v1 | ||||
|         uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0 | ||||
|         with: | ||||
|           # FOSSA Push-Only API Token | ||||
|           fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de | ||||
|           github-token: ${{ github.token }} | ||||
| 
 | ||||
|   snyk: | ||||
|     name: Snyk | ||||
|   scan-snyk: | ||||
|     runs-on: ubuntu-latest | ||||
|     if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | ||||
|     permissions: | ||||
|       security-events: write | ||||
|     if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Setup Kustomize | ||||
|         uses: fluxcd/pkg//actions/kustomize@main | ||||
|       - name: Build manifests | ||||
|         uses: fluxcd/pkg/actions/kustomize@main | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 | ||||
|         with: | ||||
|           go-version: 1.20.x | ||||
|           cache-dependency-path: | | ||||
|             **/go.sum | ||||
|             **/go.mod | ||||
|       - name: Download modules and build manifests | ||||
|         run: | | ||||
|           make tidy | ||||
|           make cmd/flux/.manifests.done | ||||
|       - name: Run Snyk to check for vulnerabilities | ||||
|         uses: snyk/actions/golang@master | ||||
|       - uses: snyk/actions/setup@b98d498629f1c368650224d6d212bf7dfa89e4bf | ||||
|       - name:  Run Snyk to check for vulnerabilities | ||||
|         continue-on-error: true | ||||
|         run: | | ||||
|           snyk test --sarif-file-output=snyk.sarif | ||||
|         env: | ||||
|           SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | ||||
|         with: | ||||
|           args: --sarif-file-output=snyk.sarif | ||||
|       - name: Upload result to GitHub Code Scanning | ||||
|         uses: github/codeql-action/upload-sarif@v1 | ||||
|         uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 | ||||
|         with: | ||||
|           sarif_file: snyk.sarif | ||||
| 
 | ||||
|   codeql: | ||||
|     name: CodeQL | ||||
|   scan-codeql: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       security-events: write | ||||
|     if: github.actor != 'dependabot[bot]' | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 | ||||
|         with: | ||||
|           go-version: 1.20.x | ||||
|           cache-dependency-path: | | ||||
|             **/go.sum | ||||
|             **/go.mod | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@v1 | ||||
|         uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 | ||||
|         with: | ||||
|           languages: go | ||||
|           # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs | ||||
|           # xref: https://codeql.github.com/codeql-query-help/go/ | ||||
|           queries: security-and-quality | ||||
|       - name: Autobuild | ||||
|         uses: github/codeql-action/autobuild@v1 | ||||
|         uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@v1 | ||||
|         uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 | ||||
|  | ||||
| @ -0,0 +1,28 @@ | ||||
| name: sync-labels | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - .github/labels.yaml | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   labels: | ||||
|     name: Run sync | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       issues: write | ||||
|     steps: | ||||
|       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | ||||
|       - uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2 | ||||
|         with: | ||||
|           # Configuration file | ||||
|           config-file: | | ||||
|             https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml | ||||
|             .github/labels.yaml | ||||
|           # Strictly declarative | ||||
|           delete-other-labels: true | ||||
| @ -1,23 +1,20 @@ | ||||
| FROM alpine:3.14 as builder | ||||
| FROM alpine:3.19 as builder | ||||
| 
 | ||||
| RUN apk add --no-cache ca-certificates curl | ||||
| 
 | ||||
| ARG ARCH=linux/amd64 | ||||
| ARG KUBECTL_VER=1.22.2 | ||||
| ARG KUBECTL_VER=1.28.4 | ||||
| 
 | ||||
| RUN curl -sL https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \ | ||||
|     -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl && \ | ||||
|     kubectl version --client=true | ||||
| 
 | ||||
| FROM alpine:3.14 as flux-cli | ||||
| 
 | ||||
| # Create minimal nsswitch.conf file to prioritize the usage of /etc/hosts over DNS queries. | ||||
| # https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-354316460 | ||||
| RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf | ||||
| FROM alpine:3.19 as flux-cli | ||||
| 
 | ||||
| RUN apk add --no-cache ca-certificates | ||||
| 
 | ||||
| COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/ | ||||
| COPY --chmod=755 flux /usr/local/bin/ | ||||
| 
 | ||||
| USER 65534:65534 | ||||
| ENTRYPOINT [ "flux" ] | ||||
|  | ||||
| @ -1,43 +1,120 @@ | ||||
| name: Setup Flux CLI | ||||
| description: A GitHub Action for running Flux commands | ||||
| author: Stefan Prodan | ||||
| description: A GitHub Action for installing the Flux CLI | ||||
| author: Flux project | ||||
| branding: | ||||
|   color: blue | ||||
|   icon: command | ||||
| inputs: | ||||
|   version: | ||||
|     description: "Flux version e.g. 0.8.0 (defaults to latest stable release)" | ||||
|     description: "Flux version e.g. 2.0.0 (defaults to latest stable release)" | ||||
|     required: false | ||||
|   arch: | ||||
|     description: "arch can be amd64, arm64 or arm" | ||||
|     required: true | ||||
|     default: "amd64" | ||||
|     required: false | ||||
|     deprecationMessage: "No longer required, action will now detect runner arch." | ||||
|   bindir: | ||||
|     description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE." | ||||
|     required: false | ||||
|   token: | ||||
|     description: "Token used to authentication against the GitHub.com API. Defaults to the token from the GitHub context of the workflow." | ||||
|     required: false | ||||
| runs: | ||||
|   using: composite | ||||
|   steps: | ||||
|     - name: "Download flux binary to tmp" | ||||
|     - name: "Download the binary to the runner's cache dir" | ||||
|       shell: bash | ||||
|       run: | | ||||
|         ARCH=${{ inputs.arch }} | ||||
|         VERSION=${{ inputs.version }} | ||||
| 
 | ||||
|         if [ -z $VERSION ]; then | ||||
|           VERSION=$(curl https://api.github.com/repos/fluxcd/flux2/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) | ||||
|         TOKEN=${{ inputs.token }} | ||||
|         if [[ -z "$TOKEN" ]]; then | ||||
|           TOKEN=${{ github.token }} | ||||
|         fi | ||||
| 
 | ||||
|         BIN_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_${ARCH}.tar.gz" | ||||
|         curl -sL ${BIN_URL} -o /tmp/flux.tar.gz | ||||
|         mkdir -p /tmp/flux | ||||
|         tar -C /tmp/flux/ -zxvf /tmp/flux.tar.gz | ||||
|     - name: "Add flux binary to /usr/local/bin" | ||||
|       shell: bash | ||||
|       run: | | ||||
|         sudo cp /tmp/flux/flux /usr/local/bin | ||||
|     - name: "Cleanup tmp" | ||||
|       shell: bash | ||||
|       run: | | ||||
|         rm -rf /tmp/flux/ /tmp/flux.tar.gz | ||||
|     - name: "Verify correct installation of binary" | ||||
|         if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then | ||||
|           VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4) | ||||
|         fi | ||||
|         if [[ -z "$VERSION" ]]; then | ||||
|           echo "Unable to determine Flux CLI version" | ||||
|           exit 1 | ||||
|         fi | ||||
|         if [[ $VERSION = v* ]]; then | ||||
|           VERSION="${VERSION:1}" | ||||
|         fi | ||||
| 
 | ||||
|         OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]') | ||||
|         if [[ "$OS" == "macos" ]]; then | ||||
|           OS="darwin" | ||||
|         fi | ||||
| 
 | ||||
|         ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]') | ||||
|         if [[ "$ARCH" == "x64" ]]; then | ||||
|           ARCH="amd64" | ||||
|         elif [[ "$ARCH" == "x86" ]]; then | ||||
|           ARCH="386" | ||||
|         fi | ||||
| 
 | ||||
|         FLUX_EXEC_FILE="flux" | ||||
|         if [[ "$OS" == "windows" ]]; then | ||||
|             FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe" | ||||
|         fi | ||||
| 
 | ||||
|         FLUX_TOOL_DIR=${{ inputs.bindir }} | ||||
|         if [[ -z "$FLUX_TOOL_DIR" ]]; then | ||||
|           FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}" | ||||
|         fi | ||||
|         if [[ ! -x "$FLUX_TOOL_DIR/FLUX_EXEC_FILE" ]]; then | ||||
|           DL_DIR="$(mktemp -dt flux2-XXXXXX)" | ||||
|           trap 'rm -rf $DL_DIR' EXIT | ||||
| 
 | ||||
|           echo "Downloading flux ${VERSION} for ${OS}/${ARCH}" | ||||
|           FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.tar.gz" | ||||
|           if [[ "$OS" == "windows" ]]; then | ||||
|             FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.zip" | ||||
|           fi | ||||
| 
 | ||||
|           FLUX_CHECKSUMS_FILE="flux_${VERSION}_checksums.txt" | ||||
| 
 | ||||
|           FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/" | ||||
| 
 | ||||
|           curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE" | ||||
|           curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE" | ||||
| 
 | ||||
|           echo "Verifying checksum" | ||||
|           sum="" | ||||
|           if command -v openssl > /dev/null; then | ||||
|             sum=$(openssl sha256 "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $2}') | ||||
|           elif command -v sha256sum > /dev/null; then | ||||
|             sum=$(sha256sum "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $1}') | ||||
|           fi | ||||
| 
 | ||||
|           if [[ -z "$sum" ]]; then | ||||
|             echo "Neither openssl nor sha256sum found. Cannot calculate checksum." | ||||
|             exit 1 | ||||
|           fi | ||||
| 
 | ||||
|           expected_sum=$(grep " $FLUX_TARGET_FILE\$" "$DL_DIR/$FLUX_CHECKSUMS_FILE" | awk '{print $1}') | ||||
|           if [ "$sum" != "$expected_sum" ]; then | ||||
|             echo "SHA sum of ${FLUX_TARGET_FILE} does not match. Aborting." | ||||
|             exit 1 | ||||
|           fi | ||||
| 
 | ||||
|           echo "Installing flux to ${FLUX_TOOL_DIR}" | ||||
|           mkdir -p "$FLUX_TOOL_DIR" | ||||
|          | ||||
|           if [[ "$OS" == "windows" ]]; then | ||||
|             unzip "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_EXEC_FILE" -d "$FLUX_TOOL_DIR" | ||||
|           else | ||||
|             tar xzf "$DL_DIR/$FLUX_TARGET_FILE" -C "$FLUX_TOOL_DIR" $FLUX_EXEC_FILE | ||||
|           fi | ||||
| 
 | ||||
|           chmod +x "$FLUX_TOOL_DIR/$FLUX_EXEC_FILE" | ||||
|         fi | ||||
| 
 | ||||
|         echo "Adding flux to path" | ||||
|         echo "$FLUX_TOOL_DIR" >> "$GITHUB_PATH" | ||||
| 
 | ||||
|     - name: "Print installed flux version" | ||||
|       shell: bash | ||||
|       run: | | ||||
|         flux -v | ||||
|  | ||||
| @ -0,0 +1,275 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/git" | ||||
| 	"github.com/fluxcd/pkg/git/gogit" | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/internal/flags" | ||||
| 	"github.com/fluxcd/flux2/v2/internal/utils" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/bootstrap" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen/install" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" | ||||
| ) | ||||
| 
 | ||||
| var bootstrapGiteaCmd = &cobra.Command{ | ||||
| 	Use:   "gitea", | ||||
| 	Short: "Deploy Flux on a cluster connected to a Gitea repository", | ||||
| 	Long: `The bootstrap gitea command creates the Gitea repository if it doesn't exists and | ||||
| commits the Flux manifests to the specified branch. | ||||
| Then it configures the target cluster to synchronize with that repository. | ||||
| If the Flux components are present on the cluster, | ||||
| the bootstrap command will perform an upgrade if needed.`, | ||||
| 	Example: `  # Create a Gitea personal access token and export it as an env var | ||||
|   export GITEA_TOKEN=<my-token> | ||||
| 
 | ||||
|   # Run bootstrap for a private repository owned by a Gitea organization | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for a private repository and assign organization teams to it | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug> --team=<team2 slug> --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for a public repository on a personal account | ||||
|   flux bootstrap gitea --owner=<user> --repository=<repository name> --private=false --personal=true --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for a private repository hosted on Gitea Enterprise using SSH auth | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for a private repository hosted on Gitea Enterprise using HTTPS auth | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster | ||||
| 
 | ||||
|   # Run bootstrap for an existing repository with a branch named main | ||||
|   flux bootstrap gitea --owner=<organization> --repository=<repository name> --branch=main --path=clusters/my-cluster`, | ||||
| 	RunE: bootstrapGiteaCmdRun, | ||||
| } | ||||
| 
 | ||||
| type giteaFlags struct { | ||||
| 	owner        string | ||||
| 	repository   string | ||||
| 	interval     time.Duration | ||||
| 	personal     bool | ||||
| 	private      bool | ||||
| 	hostname     string | ||||
| 	path         flags.SafeRelativePath | ||||
| 	teams        []string | ||||
| 	readWriteKey bool | ||||
| 	reconcile    bool | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	gtDefaultPermission = "maintain" | ||||
| 	gtDefaultDomain     = "gitea.com" | ||||
| 	gtTokenEnvVar       = "GITEA_TOKEN" | ||||
| ) | ||||
| 
 | ||||
| var giteaArgs giteaFlags | ||||
| 
 | ||||
| func init() { | ||||
| 	bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.owner, "owner", "", "Gitea user or organization name") | ||||
| 	bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.repository, "repository", "", "Gitea repository name") | ||||
| 	bootstrapGiteaCmd.Flags().StringSliceVar(&giteaArgs.teams, "team", []string{}, "Gitea team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)") | ||||
| 	bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.personal, "personal", false, "if true, the owner is assumed to be a Gitea user; otherwise an org") | ||||
| 	bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.private, "private", true, "if true, the repository is setup or configured as private") | ||||
| 	bootstrapGiteaCmd.Flags().DurationVar(&giteaArgs.interval, "interval", time.Minute, "sync interval") | ||||
| 	bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.hostname, "hostname", gtDefaultDomain, "Gitea hostname") | ||||
| 	bootstrapGiteaCmd.Flags().Var(&giteaArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") | ||||
| 	bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") | ||||
| 	bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") | ||||
| 
 | ||||
| 	bootstrapCmd.AddCommand(bootstrapGiteaCmd) | ||||
| } | ||||
| 
 | ||||
| func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { | ||||
| 	gtToken := os.Getenv(gtTokenEnvVar) | ||||
| 	if gtToken == "" { | ||||
| 		var err error | ||||
| 		gtToken, err = readPasswordFromStdin("Please enter your Gitea personal access token (PAT): ") | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("could not read token: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if err := bootstrapValidate(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||
| 	defer cancel() | ||||
| 
 | ||||
| 	kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Manifest base
 | ||||
| 	if ver, err := getVersion(bootstrapArgs.version); err != nil { | ||||
| 		return err | ||||
| 	} else { | ||||
| 		bootstrapArgs.version = ver | ||||
| 	} | ||||
| 	manifestsBase, err := buildEmbeddedManifestBase() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer os.RemoveAll(manifestsBase) | ||||
| 
 | ||||
| 	var caBundle []byte | ||||
| 	if bootstrapArgs.caFile != "" { | ||||
| 		var err error | ||||
| 		caBundle, err = os.ReadFile(bootstrapArgs.caFile) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("unable to read TLS CA file: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	// Build Gitea provider
 | ||||
| 	providerCfg := provider.Config{ | ||||
| 		Provider: provider.GitProviderGitea, | ||||
| 		Hostname: giteaArgs.hostname, | ||||
| 		Token:    gtToken, | ||||
| 		CaBundle: caBundle, | ||||
| 	} | ||||
| 	providerClient, err := provider.BuildGitProvider(providerCfg) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create temporary working dir: %w", err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} | ||||
| 	gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ | ||||
| 		Transport: git.HTTPS, | ||||
| 		Username:  giteaArgs.owner, | ||||
| 		Password:  gtToken, | ||||
| 		CAFile:    caBundle, | ||||
| 	}, clientOpts...) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create a Git client: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Install manifest config
 | ||||
| 	installOptions := install.Options{ | ||||
| 		BaseURL:                rootArgs.defaults.BaseURL, | ||||
| 		Version:                bootstrapArgs.version, | ||||
| 		Namespace:              *kubeconfigArgs.Namespace, | ||||
| 		Components:             bootstrapComponents(), | ||||
| 		Registry:               bootstrapArgs.registry, | ||||
| 		ImagePullSecret:        bootstrapArgs.imagePullSecret, | ||||
| 		WatchAllNamespaces:     bootstrapArgs.watchAllNamespaces, | ||||
| 		NetworkPolicy:          bootstrapArgs.networkPolicy, | ||||
| 		LogLevel:               bootstrapArgs.logLevel.String(), | ||||
| 		NotificationController: rootArgs.defaults.NotificationController, | ||||
| 		ManifestFile:           rootArgs.defaults.ManifestFile, | ||||
| 		Timeout:                rootArgs.timeout, | ||||
| 		TargetPath:             giteaArgs.path.ToSlash(), | ||||
| 		ClusterDomain:          bootstrapArgs.clusterDomain, | ||||
| 		TolerationKeys:         bootstrapArgs.tolerationKeys, | ||||
| 	} | ||||
| 	if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { | ||||
| 		installOptions.BaseURL = customBaseURL | ||||
| 	} | ||||
| 
 | ||||
| 	// Source generation and secret config
 | ||||
| 	secretOpts := sourcesecret.Options{ | ||||
| 		Name:         bootstrapArgs.secretName, | ||||
| 		Namespace:    *kubeconfigArgs.Namespace, | ||||
| 		TargetPath:   giteaArgs.path.ToSlash(), | ||||
| 		ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, | ||||
| 	} | ||||
| 	if bootstrapArgs.tokenAuth { | ||||
| 		secretOpts.Username = "git" | ||||
| 		secretOpts.Password = gtToken | ||||
| 		secretOpts.CAFile = caBundle | ||||
| 	} else { | ||||
| 		secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) | ||||
| 		secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) | ||||
| 		secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve | ||||
| 
 | ||||
| 		secretOpts.SSHHostname = giteaArgs.hostname | ||||
| 		if bootstrapArgs.sshHostname != "" { | ||||
| 			secretOpts.SSHHostname = bootstrapArgs.sshHostname | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Sync manifest config
 | ||||
| 	syncOpts := sync.Options{ | ||||
| 		Interval:          giteaArgs.interval, | ||||
| 		Name:              *kubeconfigArgs.Namespace, | ||||
| 		Namespace:         *kubeconfigArgs.Namespace, | ||||
| 		Branch:            bootstrapArgs.branch, | ||||
| 		Secret:            bootstrapArgs.secretName, | ||||
| 		TargetPath:        giteaArgs.path.ToSlash(), | ||||
| 		ManifestFile:      sync.MakeDefaultOptions().ManifestFile, | ||||
| 		RecurseSubmodules: bootstrapArgs.recurseSubmodules, | ||||
| 	} | ||||
| 
 | ||||
| 	entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Bootstrap config
 | ||||
| 	bootstrapOpts := []bootstrap.GitProviderOption{ | ||||
| 		bootstrap.WithProviderRepository(giteaArgs.owner, giteaArgs.repository, giteaArgs.personal), | ||||
| 		bootstrap.WithBranch(bootstrapArgs.branch), | ||||
| 		bootstrap.WithBootstrapTransportType("https"), | ||||
| 		bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), | ||||
| 		bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), | ||||
| 		bootstrap.WithProviderTeamPermissions(mapTeamSlice(giteaArgs.teams, gtDefaultPermission)), | ||||
| 		bootstrap.WithReadWriteKeyPermissions(giteaArgs.readWriteKey), | ||||
| 		bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), | ||||
| 		bootstrap.WithLogger(logger), | ||||
| 		bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), | ||||
| 	} | ||||
| 	if bootstrapArgs.sshHostname != "" { | ||||
| 		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) | ||||
| 	} | ||||
| 	if bootstrapArgs.tokenAuth { | ||||
| 		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) | ||||
| 	} | ||||
| 	if !giteaArgs.private { | ||||
| 		bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) | ||||
| 	} | ||||
| 	if giteaArgs.reconcile { | ||||
| 		bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Setup bootstrapper with constructed configs
 | ||||
| 	b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Run
 | ||||
| 	return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) | ||||
| } | ||||
| @ -0,0 +1,117 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	oci "github.com/fluxcd/pkg/oci/client" | ||||
| 	"github.com/fluxcd/pkg/sourceignore" | ||||
| ) | ||||
| 
 | ||||
| var buildArtifactCmd = &cobra.Command{ | ||||
| 	Use:   "artifact", | ||||
| 	Short: "Build artifact", | ||||
| 	Long: withPreviewNote(`The build artifact command creates a tgz file with the manifests | ||||
| from the given directory or a single manifest file.`), | ||||
| 	Example: `  # Build the given manifests directory into an artifact | ||||
|   flux build artifact --path ./path/to/local/manifests --output ./path/to/artifact.tgz | ||||
| 
 | ||||
|   # Build the given single manifest file into an artifact | ||||
|   flux build artifact --path ./path/to/local/manifest.yaml --output ./path/to/artifact.tgz | ||||
| 
 | ||||
|   # List the files bundled in the artifact | ||||
|   tar -ztvf ./path/to/artifact.tgz | ||||
| `, | ||||
| 	RunE: buildArtifactCmdRun, | ||||
| } | ||||
| 
 | ||||
| type buildArtifactFlags struct { | ||||
| 	output      string | ||||
| 	path        string | ||||
| 	ignorePaths []string | ||||
| } | ||||
| 
 | ||||
| var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) | ||||
| 
 | ||||
| var buildArtifactArgs buildArtifactFlags | ||||
| 
 | ||||
| func init() { | ||||
| 	buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.") | ||||
| 	buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.") | ||||
| 	buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") | ||||
| 
 | ||||
| 	buildCmd.AddCommand(buildArtifactCmd) | ||||
| } | ||||
| 
 | ||||
| func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { | ||||
| 	if buildArtifactArgs.path == "" { | ||||
| 		return fmt.Errorf("invalid path %q", buildArtifactArgs.path) | ||||
| 	} | ||||
| 
 | ||||
| 	path := buildArtifactArgs.path | ||||
| 	var err error | ||||
| 	if buildArtifactArgs.path == "-" { | ||||
| 		path, err = saveReaderToFile(os.Stdin) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		defer os.Remove(path) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := os.Stat(path); err != nil { | ||||
| 		return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path) | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Actionf("building artifact from %s", path) | ||||
| 
 | ||||
| 	ociClient := oci.NewClient(oci.DefaultOptions()) | ||||
| 	if err := ociClient.Build(buildArtifactArgs.output, path, buildArtifactArgs.ignorePaths); err != nil { | ||||
| 		return fmt.Errorf("building artifact failed, error: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Successf("artifact created at %s", buildArtifactArgs.output) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func saveReaderToFile(reader io.Reader) (string, error) { | ||||
| 	b, err := io.ReadAll(bufio.NewReader(reader)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	b = bytes.TrimRight(b, "\r\n") | ||||
| 	f, err := os.CreateTemp("", "*.yaml") | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("unable to create temp dir for stdin") | ||||
| 	} | ||||
| 
 | ||||
| 	defer f.Close() | ||||
| 
 | ||||
| 	if _, err := f.Write(b); err != nil { | ||||
| 		return "", fmt.Errorf("error writing stdin to file: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return f.Name(), nil | ||||
| } | ||||
| @ -0,0 +1,70 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	. "github.com/onsi/gomega" | ||||
| ) | ||||
| 
 | ||||
| func Test_saveReaderToFile(t *testing.T) { | ||||
| 	g := NewWithT(t) | ||||
| 
 | ||||
| 	testString := `apiVersion: v1 | ||||
| kind: ConfigMap | ||||
| metadata: | ||||
|   name: myapp | ||||
| data: | ||||
|   foo: bar` | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name      string | ||||
| 		string    string | ||||
| 		expectErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:   "yaml", | ||||
| 			string: testString, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "yaml with carriage return", | ||||
| 			string: testString + "\r\n", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			tmpFile, err := saveReaderToFile(strings.NewReader(tt.string)) | ||||
| 			g.Expect(err).To(BeNil()) | ||||
| 
 | ||||
| 			t.Cleanup(func() { _ = os.Remove(tmpFile) }) | ||||
| 
 | ||||
| 			b, err := os.ReadFile(tmpFile) | ||||
| 			if tt.expectErr { | ||||
| 				g.Expect(err).To(Not(BeNil())) | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			g.Expect(err).To(BeNil()) | ||||
| 			g.Expect(string(b)).To(BeEquivalentTo(testString)) | ||||
| 		}) | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,156 @@ | ||||
| /* | ||||
| Copyright 2021 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/ssa" | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/internal/build" | ||||
| ) | ||||
| 
 | ||||
| var buildKsCmd = &cobra.Command{ | ||||
| 	Use:     "kustomization", | ||||
| 	Aliases: []string{"ks"}, | ||||
| 	Short:   "Build Kustomization", | ||||
| 	Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization.  | ||||
| It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml | ||||
| pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout. | ||||
| 
 | ||||
| It is possible to specify a Flux kustomization file using --kustomization-file.`, | ||||
| 	Example: `# Build the local manifests as they were built on the cluster | ||||
| flux build kustomization my-app --path ./path/to/local/manifests | ||||
| 
 | ||||
| # Build using a local flux kustomization file | ||||
| flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml | ||||
| 
 | ||||
| # Build in dry-run mode without connecting to the cluster. | ||||
| # Note that variable substitutions from Secrets and ConfigMaps are skipped in dry-run mode. | ||||
| flux build kustomization my-app --path ./path/to/local/manifests \ | ||||
| 	--kustomization-file ./path/to/local/my-app.yaml \ | ||||
| 	--dry-run | ||||
| 
 | ||||
| # Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. | ||||
| flux build kustomization my-app --path ./path/to/local/manifests \ | ||||
| 	--kustomization-file ./path/to/local/my-app.yaml \ | ||||
| 	--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, | ||||
| 	ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), | ||||
| 	RunE:              buildKsCmdRun, | ||||
| } | ||||
| 
 | ||||
| type buildKsFlags struct { | ||||
| 	kustomizationFile string | ||||
| 	path              string | ||||
| 	ignorePaths       []string | ||||
| 	dryRun            bool | ||||
| } | ||||
| 
 | ||||
| var buildKsArgs buildKsFlags | ||||
| 
 | ||||
| func init() { | ||||
| 	buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") | ||||
| 	buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") | ||||
| 	buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") | ||||
| 	buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") | ||||
| 	buildCmd.AddCommand(buildKsCmd) | ||||
| } | ||||
| 
 | ||||
| func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { | ||||
| 	if len(args) < 1 { | ||||
| 		return fmt.Errorf("%s name is required", kustomizationType.humanKind) | ||||
| 	} | ||||
| 	name := args[0] | ||||
| 
 | ||||
| 	if buildKsArgs.path == "" { | ||||
| 		return fmt.Errorf("invalid resource path %q", buildKsArgs.path) | ||||
| 	} | ||||
| 
 | ||||
| 	if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { | ||||
| 		return fmt.Errorf("invalid resource path %q", buildKsArgs.path) | ||||
| 	} | ||||
| 
 | ||||
| 	if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" { | ||||
| 		return fmt.Errorf("dry-run mode requires a kustomization file") | ||||
| 	} | ||||
| 
 | ||||
| 	if buildKsArgs.kustomizationFile != "" { | ||||
| 		if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { | ||||
| 			return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var builder *build.Builder | ||||
| 	if buildKsArgs.dryRun { | ||||
| 		builder, err = build.NewBuilder(name, buildKsArgs.path, | ||||
| 			build.WithTimeout(rootArgs.timeout), | ||||
| 			build.WithKustomizationFile(buildKsArgs.kustomizationFile), | ||||
| 			build.WithDryRun(buildKsArgs.dryRun), | ||||
| 			build.WithNamespace(*kubeconfigArgs.Namespace), | ||||
| 			build.WithIgnore(buildKsArgs.ignorePaths), | ||||
| 		) | ||||
| 	} else { | ||||
| 		builder, err = build.NewBuilder(name, buildKsArgs.path, | ||||
| 			build.WithClientConfig(kubeconfigArgs, kubeclientOptions), | ||||
| 			build.WithTimeout(rootArgs.timeout), | ||||
| 			build.WithKustomizationFile(buildKsArgs.kustomizationFile), | ||||
| 			build.WithIgnore(buildKsArgs.ignorePaths), | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// create a signal channel
 | ||||
| 	sigc := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigc, os.Interrupt) | ||||
| 
 | ||||
| 	errChan := make(chan error) | ||||
| 	go func() { | ||||
| 		objects, err := builder.Build() | ||||
| 		if err != nil { | ||||
| 			errChan <- err | ||||
| 		} | ||||
| 
 | ||||
| 		manifests, err := ssa.ObjectsToYAML(objects) | ||||
| 		if err != nil { | ||||
| 			errChan <- err | ||||
| 		} | ||||
| 
 | ||||
| 		cmd.Print(manifests) | ||||
| 		errChan <- nil | ||||
| 	}() | ||||
| 
 | ||||
| 	select { | ||||
| 	case <-sigc: | ||||
| 		fmt.Println("Build cancelled... exiting.") | ||||
| 		return builder.Cancel() | ||||
| 	case err := <-errChan: | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,202 @@ | ||||
| //go:build unit
 | ||||
| // +build unit
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2021 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"text/template" | ||||
| ) | ||||
| 
 | ||||
| func setup(t *testing.T, tmpl map[string]string) { | ||||
| 	t.Helper() | ||||
| 	testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) | ||||
| 	testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t) | ||||
| } | ||||
| 
 | ||||
| func TestBuildKustomization(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		args       string | ||||
| 		resultFile string | ||||
| 		assertFunc string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "no args", | ||||
| 			args:       "build kustomization podinfo", | ||||
| 			resultFile: "invalid resource path \"\"", | ||||
| 			assertFunc: "assertError", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build podinfo", | ||||
| 			args:       "build kustomization podinfo --path ./testdata/build-kustomization/podinfo", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build podinfo without service", | ||||
| 			args:       "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build deployment and configmap with var substitution", | ||||
| 			args:       "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build ignore", | ||||
| 			args:       "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\"", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	tmpl := map[string]string{ | ||||
| 		"fluxns": allocateNamespace("flux-system"), | ||||
| 	} | ||||
| 	setup(t, tmpl) | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var assert assertFunc | ||||
| 
 | ||||
| 			switch tt.assertFunc { | ||||
| 			case "assertGoldenTemplateFile": | ||||
| 				assert = assertGoldenTemplateFile(tt.resultFile, tmpl) | ||||
| 			case "assertError": | ||||
| 				assert = assertError(tt.resultFile) | ||||
| 			} | ||||
| 
 | ||||
| 			cmd := cmdTestCase{ | ||||
| 				args:   tt.args + " -n " + tmpl["fluxns"], | ||||
| 				assert: assert, | ||||
| 			} | ||||
| 
 | ||||
| 			cmd.runTestCmd(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestBuildLocalKustomization(t *testing.T) { | ||||
| 	podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1 | ||||
| kind: Kustomization | ||||
| metadata: | ||||
|   name: podinfo | ||||
|   namespace: {{ .fluxns }} | ||||
| spec: | ||||
|   interval: 5m0s | ||||
|   path: ./kustomize | ||||
|   force: true | ||||
|   prune: true | ||||
|   sourceRef: | ||||
|     kind: GitRepository | ||||
|     name: podinfo | ||||
|   targetNamespace: default | ||||
|   postBuild: | ||||
|     substitute: | ||||
|       cluster_env: "prod" | ||||
|       cluster_region: "eu-central-1" | ||||
| ` | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		args       string | ||||
| 		resultFile string | ||||
| 		assertFunc string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "no args", | ||||
| 			args:       "build kustomization podinfo --kustomization-file ./wrongfile/ --path ./testdata/build-kustomization/podinfo", | ||||
| 			resultFile: "invalid kustomization file \"./wrongfile/\"", | ||||
| 			assertFunc: "assertError", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build podinfo", | ||||
| 			args:       "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/podinfo", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build podinfo without service", | ||||
| 			args:       "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/delete-service", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build deployment and configmap with var substitution", | ||||
| 			args:       "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "build deployment and configmap with var substitution in dry-run mode", | ||||
| 			args:       "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution --dry-run", | ||||
| 			resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	tmpl := map[string]string{ | ||||
| 		"fluxns": allocateNamespace("flux-system"), | ||||
| 	} | ||||
| 	setup(t, tmpl) | ||||
| 
 | ||||
| 	testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) | ||||
| 
 | ||||
| 	temp, err := template.New("podinfo").Parse(podinfo) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	var b bytes.Buffer | ||||
| 	err = temp.Execute(&b, tmpl) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = os.WriteFile("./testdata/build-kustomization/podinfo.yaml", b.Bytes(), 0666) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	t.Cleanup(func() { _ = os.Remove("./testdata/build-kustomization/podinfo.yaml") }) | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var assert assertFunc | ||||
| 
 | ||||
| 			switch tt.assertFunc { | ||||
| 			case "assertGoldenTemplateFile": | ||||
| 				assert = assertGoldenTemplateFile(tt.resultFile, tmpl) | ||||
| 			case "assertError": | ||||
| 				assert = assertError(tt.resultFile) | ||||
| 			} | ||||
| 
 | ||||
| 			cmd := cmdTestCase{ | ||||
| 				args:   tt.args + " -n " + tmpl["fluxns"], | ||||
| 				assert: assert, | ||||
| 			} | ||||
| 
 | ||||
| 			cmd.runTestCmd(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,126 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/manifoldco/promptui" | ||||
| 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 
 | ||||
| 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" | ||||
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen" | ||||
| ) | ||||
| 
 | ||||
| // bootstrapLabels are labels put on a resource by kustomize-controller. These labels on the CRD indicates
 | ||||
| // that flux has been bootstrapped.
 | ||||
| var bootstrapLabels = []string{ | ||||
| 	fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group), | ||||
| 	fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group), | ||||
| } | ||||
| 
 | ||||
| // fluxClusterInfo contains information about an existing flux installation on a cluster.
 | ||||
| type fluxClusterInfo struct { | ||||
| 	// bootstrapped indicates that Flux was installed using the `flux bootstrap` command.
 | ||||
| 	bootstrapped bool | ||||
| 	// managedBy is the name of the tool being used to manage the installation of Flux.
 | ||||
| 	managedBy string | ||||
| 	// partOf indicates which distribution the instance is a part of.
 | ||||
| 	partOf string | ||||
| 	// version is the Flux version number in semver format.
 | ||||
| 	version string | ||||
| } | ||||
| 
 | ||||
| // getFluxClusterInfo returns information on the Flux installation running on the cluster.
 | ||||
| // If an error occurred, the returned error will be non-nil.
 | ||||
| //
 | ||||
| // This function retrieves the GitRepository CRD from the cluster and checks it
 | ||||
| // for a set of labels used to determine the Flux version and how Flux was installed.
 | ||||
| // It returns the NotFound error from the underlying library if it was unable to find
 | ||||
| // the GitRepository CRD and this can be used to check if Flux is installed.
 | ||||
| func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) { | ||||
| 	var info fluxClusterInfo | ||||
| 	crdMetadata := &metav1.PartialObjectMetadata{ | ||||
| 		TypeMeta: metav1.TypeMeta{ | ||||
| 			APIVersion: apiextensionsv1.SchemeGroupVersion.String(), | ||||
| 			Kind:       "CustomResourceDefinition", | ||||
| 		}, | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: fmt.Sprintf("gitrepositories.%s", sourcev1.GroupVersion.Group), | ||||
| 		}, | ||||
| 	} | ||||
| 	if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil { | ||||
| 		return info, err | ||||
| 	} | ||||
| 
 | ||||
| 	info.version = crdMetadata.Labels[manifestgen.VersionLabelKey] | ||||
| 
 | ||||
| 	var present bool | ||||
| 	for _, l := range bootstrapLabels { | ||||
| 		_, present = crdMetadata.Labels[l] | ||||
| 	} | ||||
| 	if present { | ||||
| 		info.bootstrapped = true | ||||
| 	} | ||||
| 
 | ||||
| 	// the `app.kubernetes.io/managed-by` label is not set by flux but might be set by other
 | ||||
| 	// tools used to install Flux e.g Helm.
 | ||||
| 	if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok { | ||||
| 		info.managedBy = manager | ||||
| 	} | ||||
| 
 | ||||
| 	if partOf, ok := crdMetadata.Labels[manifestgen.PartOfLabelKey]; ok { | ||||
| 		info.partOf = partOf | ||||
| 	} | ||||
| 	return info, nil | ||||
| } | ||||
| 
 | ||||
| // confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding
 | ||||
| // a Flux installation. It returns nil if the installation should continue,
 | ||||
| // promptui.ErrAbort if the user doesn't confirm, or an error encountered.
 | ||||
| func confirmFluxInstallOverride(info fluxClusterInfo) error { | ||||
| 	// no need to display prompt if installation is managed by Flux
 | ||||
| 	if installManagedByFlux(info.managedBy) { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy) | ||||
| 	fmt.Fprintln(rootCmd.ErrOrStderr(), display) | ||||
| 	prompt := promptui.Prompt{ | ||||
| 		Label:     fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy), | ||||
| 		IsConfirm: true, | ||||
| 	} | ||||
| 	_, err := prompt.Run() | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (info fluxClusterInfo) distribution() string { | ||||
| 	distribution := info.version | ||||
| 	if info.partOf != "" { | ||||
| 		distribution = fmt.Sprintf("%s-%s", info.partOf, info.version) | ||||
| 	} | ||||
| 	return distribution | ||||
| } | ||||
| 
 | ||||
| func installManagedByFlux(manager string) bool { | ||||
| 	return manager == "" || manager == "flux" | ||||
| } | ||||
| @ -0,0 +1,141 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	. "github.com/onsi/gomega" | ||||
| 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/errors" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||
| 
 | ||||
| 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" | ||||
| 	"github.com/fluxcd/pkg/ssa" | ||||
| ) | ||||
| 
 | ||||
| func Test_getFluxClusterInfo(t *testing.T) { | ||||
| 	g := NewWithT(t) | ||||
| 	f, err := os.Open("./testdata/cluster_info/gitrepositories.yaml") | ||||
| 	g.Expect(err).To(BeNil()) | ||||
| 
 | ||||
| 	objs, err := ssa.ReadObjects(f) | ||||
| 	g.Expect(err).To(Not(HaveOccurred())) | ||||
| 	gitrepo := objs[0] | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		labels   map[string]string | ||||
| 		wantErr  bool | ||||
| 		wantInfo fluxClusterInfo | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:    "no git repository CRD present", | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CRD with kustomize-controller labels", | ||||
| 			labels: map[string]string{ | ||||
| 				fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group):      "flux-system", | ||||
| 				fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", | ||||
| 				"app.kubernetes.io/version":                                 "v2.1.0", | ||||
| 			}, | ||||
| 			wantInfo: fluxClusterInfo{ | ||||
| 				version:      "v2.1.0", | ||||
| 				bootstrapped: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CRD with kustomize-controller labels and managed-by label", | ||||
| 			labels: map[string]string{ | ||||
| 				fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group):      "flux-system", | ||||
| 				fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", | ||||
| 				"app.kubernetes.io/version":                                 "v2.1.0", | ||||
| 				"app.kubernetes.io/managed-by":                              "flux", | ||||
| 			}, | ||||
| 			wantInfo: fluxClusterInfo{ | ||||
| 				version:      "v2.1.0", | ||||
| 				bootstrapped: true, | ||||
| 				managedBy:    "flux", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CRD with only managed-by label", | ||||
| 			labels: map[string]string{ | ||||
| 				"app.kubernetes.io/version":    "v2.1.0", | ||||
| 				"app.kubernetes.io/managed-by": "helm", | ||||
| 			}, | ||||
| 			wantInfo: fluxClusterInfo{ | ||||
| 				version:   "v2.1.0", | ||||
| 				managedBy: "helm", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "CRD with no labels", | ||||
| 			labels:   map[string]string{}, | ||||
| 			wantInfo: fluxClusterInfo{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CRD with only version label", | ||||
| 			labels: map[string]string{ | ||||
| 				"app.kubernetes.io/version": "v2.1.0", | ||||
| 			}, | ||||
| 			wantInfo: fluxClusterInfo{ | ||||
| 				version: "v2.1.0", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CRD with version and part-of labels", | ||||
| 			labels: map[string]string{ | ||||
| 				"app.kubernetes.io/version": "v2.1.0", | ||||
| 				"app.kubernetes.io/part-of": "flux", | ||||
| 			}, | ||||
| 			wantInfo: fluxClusterInfo{ | ||||
| 				version: "v2.1.0", | ||||
| 				partOf:  "flux", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			g := NewWithT(t) | ||||
| 			newscheme := runtime.NewScheme() | ||||
| 			apiextensionsv1.AddToScheme(newscheme) | ||||
| 			builder := fake.NewClientBuilder().WithScheme(newscheme) | ||||
| 			if tt.labels != nil { | ||||
| 				gitrepo.SetLabels(tt.labels) | ||||
| 				builder = builder.WithRuntimeObjects(gitrepo) | ||||
| 			} | ||||
| 
 | ||||
| 			client := builder.Build() | ||||
| 			info, err := getFluxClusterInfo(context.Background(), client) | ||||
| 			if tt.wantErr { | ||||
| 				g.Expect(err).To(HaveOccurred()) | ||||
| 				g.Expect(errors.IsNotFound(err)).To(BeTrue()) | ||||
| 			} else { | ||||
| 				g.Expect(err).To(Not(HaveOccurred())) | ||||
| 			} | ||||
| 
 | ||||
| 			g.Expect(info).To(BeEquivalentTo(tt.wantInfo)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,121 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/internal/utils" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" | ||||
| 	"github.com/google/go-containerregistry/pkg/name" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
| 
 | ||||
| var createSecretOCICmd = &cobra.Command{ | ||||
| 	Use:   "oci [name]", | ||||
| 	Short: "Create or update a Kubernetes image pull secret", | ||||
| 	Long:  withPreviewNote(`The create secret oci command generates a Kubernetes secret that can be used for OCIRepository authentication`), | ||||
| 	Example: `  # Create an OCI authentication secret on disk and encrypt it with Mozilla SOPS | ||||
|   flux create secret oci podinfo-auth \ | ||||
|     --url=ghcr.io \ | ||||
|     --username=username \ | ||||
|     --password=password \ | ||||
|     --export > repo-auth.yaml | ||||
| 
 | ||||
|   sops --encrypt --encrypted-regex '^(data|stringData)$' \ | ||||
|     --in-place repo-auth.yaml | ||||
| 	`, | ||||
| 	RunE: createSecretOCICmdRun, | ||||
| } | ||||
| 
 | ||||
| type secretOCIFlags struct { | ||||
| 	url      string | ||||
| 	password string | ||||
| 	username string | ||||
| } | ||||
| 
 | ||||
| var secretOCIArgs = secretOCIFlags{} | ||||
| 
 | ||||
| func init() { | ||||
| 	createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts") | ||||
| 	createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username") | ||||
| 	createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication password") | ||||
| 
 | ||||
| 	createSecretCmd.AddCommand(createSecretOCICmd) | ||||
| } | ||||
| 
 | ||||
| func createSecretOCICmdRun(cmd *cobra.Command, args []string) error { | ||||
| 	if len(args) < 1 { | ||||
| 		return fmt.Errorf("name is required") | ||||
| 	} | ||||
| 
 | ||||
| 	secretName := args[0] | ||||
| 
 | ||||
| 	if secretOCIArgs.url == "" { | ||||
| 		return fmt.Errorf("--url is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if secretOCIArgs.username == "" { | ||||
| 		return fmt.Errorf("--username is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if secretOCIArgs.password == "" { | ||||
| 		return fmt.Errorf("--password is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := name.ParseReference(secretOCIArgs.url); err != nil { | ||||
| 		return fmt.Errorf("error parsing url: '%s'", err) | ||||
| 	} | ||||
| 
 | ||||
| 	opts := sourcesecret.Options{ | ||||
| 		Name:      secretName, | ||||
| 		Namespace: *kubeconfigArgs.Namespace, | ||||
| 		Registry:  secretOCIArgs.url, | ||||
| 		Password:  secretOCIArgs.password, | ||||
| 		Username:  secretOCIArgs.username, | ||||
| 	} | ||||
| 
 | ||||
| 	secret, err := sourcesecret.Generate(opts) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if createArgs.export { | ||||
| 		rootCmd.Println(secret.Content) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||
| 	defer cancel() | ||||
| 	kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	var s corev1.Secret | ||||
| 	if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := upsertSecret(ctx, kubeClient, s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Actionf("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) | ||||
| 	return nil | ||||
| } | ||||
| @ -0,0 +1,81 @@ | ||||
| //go:build unit
 | ||||
| // +build unit
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestCreateSourceHelm(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		args       string | ||||
| 		resultFile string | ||||
| 		assertFunc string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "no args", | ||||
| 			args:       "create source helm", | ||||
| 			resultFile: "name is required", | ||||
| 			assertFunc: "assertError", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "OCI repo", | ||||
| 			args:       "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export", | ||||
| 			resultFile: "./testdata/create_source_helm/oci.golden", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "OCI repo with Secret ref", | ||||
| 			args:       "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --secret-ref=creds --export", | ||||
| 			resultFile: "./testdata/create_source_helm/oci-with-secret.golden", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "HTTPS repo", | ||||
| 			args:       "create source helm podinfo --url=https://stefanprodan.github.io/charts/podinfo --interval 5m --export", | ||||
| 			resultFile: "./testdata/create_source_helm/https.golden", | ||||
| 			assertFunc: "assertGoldenTemplateFile", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	tmpl := map[string]string{ | ||||
| 		"fluxns": allocateNamespace("flux-system"), | ||||
| 	} | ||||
| 	setup(t, tmpl) | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var assert assertFunc | ||||
| 			switch tt.assertFunc { | ||||
| 			case "assertGoldenTemplateFile": | ||||
| 				assert = assertGoldenTemplateFile(tt.resultFile, tmpl) | ||||
| 			case "assertError": | ||||
| 				assert = assertError(tt.resultFile) | ||||
| 			} | ||||
| 
 | ||||
| 			cmd := cmdTestCase{ | ||||
| 				args:   tt.args + " -n " + tmpl["fluxns"], | ||||
| 				assert: assert, | ||||
| 			} | ||||
| 			cmd.runTestCmd(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,236 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"k8s.io/apimachinery/pkg/api/errors" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/apis/meta" | ||||
| 
 | ||||
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/internal/flags" | ||||
| 	"github.com/fluxcd/flux2/v2/internal/utils" | ||||
| ) | ||||
| 
 | ||||
| var createSourceOCIRepositoryCmd = &cobra.Command{ | ||||
| 	Use:   "oci [name]", | ||||
| 	Short: "Create or update an OCIRepository", | ||||
| 	Long:  withPreviewNote(`The create source oci command generates an OCIRepository resource and waits for it to be ready.`), | ||||
| 	Example: `  # Create an OCIRepository for a public container image | ||||
|   flux create source oci podinfo \ | ||||
|     --url=oci://ghcr.io/stefanprodan/manifests/podinfo \
 | ||||
|     --tag=6.1.6 \ | ||||
|     --interval=10m | ||||
| `, | ||||
| 	RunE: createSourceOCIRepositoryCmdRun, | ||||
| } | ||||
| 
 | ||||
| type sourceOCIRepositoryFlags struct { | ||||
| 	url             string | ||||
| 	tag             string | ||||
| 	semver          string | ||||
| 	digest          string | ||||
| 	secretRef       string | ||||
| 	serviceAccount  string | ||||
| 	certSecretRef   string | ||||
| 	verifyProvider  flags.SourceOCIVerifyProvider | ||||
| 	verifySecretRef string | ||||
| 	ignorePaths     []string | ||||
| 	provider        flags.SourceOCIProvider | ||||
| 	insecure        bool | ||||
| } | ||||
| 
 | ||||
| var sourceOCIRepositoryArgs = newSourceOCIFlags() | ||||
| 
 | ||||
| func newSourceOCIFlags() sourceOCIRepositoryFlags { | ||||
| 	return sourceOCIRepositoryFlags{ | ||||
| 		provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates") | ||||
| 	createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description()) | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification") | ||||
| 	createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") | ||||
| 	createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") | ||||
| 
 | ||||
| 	createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) | ||||
| } | ||||
| 
 | ||||
| func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { | ||||
| 	name := args[0] | ||||
| 
 | ||||
| 	if sourceOCIRepositoryArgs.url == "" { | ||||
| 		return fmt.Errorf("url is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" { | ||||
| 		return fmt.Errorf("--tag, --tag-semver or --digest is required") | ||||
| 	} | ||||
| 
 | ||||
| 	sourceLabels, err := parseLabels() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	var ignorePaths *string | ||||
| 	if len(sourceOCIRepositoryArgs.ignorePaths) > 0 { | ||||
| 		ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n") | ||||
| 		ignorePaths = &ignorePathsStr | ||||
| 	} | ||||
| 
 | ||||
| 	repository := &sourcev1.OCIRepository{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name:      name, | ||||
| 			Namespace: *kubeconfigArgs.Namespace, | ||||
| 			Labels:    sourceLabels, | ||||
| 		}, | ||||
| 		Spec: sourcev1.OCIRepositorySpec{ | ||||
| 			Provider: sourceOCIRepositoryArgs.provider.String(), | ||||
| 			URL:      sourceOCIRepositoryArgs.url, | ||||
| 			Insecure: sourceOCIRepositoryArgs.insecure, | ||||
| 			Interval: metav1.Duration{ | ||||
| 				Duration: createArgs.interval, | ||||
| 			}, | ||||
| 			Reference: &sourcev1.OCIRepositoryRef{}, | ||||
| 			Ignore:    ignorePaths, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	if digest := sourceOCIRepositoryArgs.digest; digest != "" { | ||||
| 		repository.Spec.Reference.Digest = digest | ||||
| 	} | ||||
| 	if semver := sourceOCIRepositoryArgs.semver; semver != "" { | ||||
| 		repository.Spec.Reference.SemVer = semver | ||||
| 	} | ||||
| 	if tag := sourceOCIRepositoryArgs.tag; tag != "" { | ||||
| 		repository.Spec.Reference.Tag = tag | ||||
| 	} | ||||
| 
 | ||||
| 	if createSourceArgs.fetchTimeout > 0 { | ||||
| 		repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} | ||||
| 	} | ||||
| 
 | ||||
| 	if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" { | ||||
| 		repository.Spec.ServiceAccountName = saName | ||||
| 	} | ||||
| 
 | ||||
| 	if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" { | ||||
| 		repository.Spec.SecretRef = &meta.LocalObjectReference{ | ||||
| 			Name: secretName, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if secretName := sourceOCIRepositoryArgs.certSecretRef; secretName != "" { | ||||
| 		repository.Spec.CertSecretRef = &meta.LocalObjectReference{ | ||||
| 			Name: secretName, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if provider := sourceOCIRepositoryArgs.verifyProvider.String(); provider != "" { | ||||
| 		repository.Spec.Verify = &sourcev1.OCIRepositoryVerification{ | ||||
| 			Provider: provider, | ||||
| 		} | ||||
| 		if secretName := sourceOCIRepositoryArgs.verifySecretRef; secretName != "" { | ||||
| 			repository.Spec.Verify.SecretRef = &meta.LocalObjectReference{ | ||||
| 				Name: secretName, | ||||
| 			} | ||||
| 		} | ||||
| 	} else if sourceOCIRepositoryArgs.verifySecretRef != "" { | ||||
| 		return fmt.Errorf("a verification provider must be specified when a secret is specified") | ||||
| 	} | ||||
| 
 | ||||
| 	if createArgs.export { | ||||
| 		return printExport(exportOCIRepository(repository)) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||
| 	defer cancel() | ||||
| 
 | ||||
| 	kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Actionf("applying OCIRepository") | ||||
| 	namespacedName, err := upsertOCIRepository(ctx, kubeClient, repository) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Waitingf("waiting for OCIRepository reconciliation") | ||||
| 	if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, | ||||
| 		isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	logger.Successf("OCIRepository reconciliation completed") | ||||
| 
 | ||||
| 	if repository.Status.Artifact == nil { | ||||
| 		return fmt.Errorf("no artifact was found") | ||||
| 	} | ||||
| 	logger.Successf("fetched revision: %s", repository.Status.Artifact.Revision) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func upsertOCIRepository(ctx context.Context, kubeClient client.Client, | ||||
| 	ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) { | ||||
| 	namespacedName := types.NamespacedName{ | ||||
| 		Namespace: ociRepository.GetNamespace(), | ||||
| 		Name:      ociRepository.GetName(), | ||||
| 	} | ||||
| 
 | ||||
| 	var existing sourcev1.OCIRepository | ||||
| 	err := kubeClient.Get(ctx, namespacedName, &existing) | ||||
| 	if err != nil { | ||||
| 		if errors.IsNotFound(err) { | ||||
| 			if err := kubeClient.Create(ctx, ociRepository); err != nil { | ||||
| 				return namespacedName, err | ||||
| 			} else { | ||||
| 				logger.Successf("OCIRepository created") | ||||
| 				return namespacedName, nil | ||||
| 			} | ||||
| 		} | ||||
| 		return namespacedName, err | ||||
| 	} | ||||
| 
 | ||||
| 	existing.Labels = ociRepository.Labels | ||||
| 	existing.Spec = ociRepository.Spec | ||||
| 	if err := kubeClient.Update(ctx, &existing); err != nil { | ||||
| 		return namespacedName, err | ||||
| 	} | ||||
| 	ociRepository = &existing | ||||
| 	logger.Successf("OCIRepository updated") | ||||
| 	return namespacedName, nil | ||||
| } | ||||
| @ -0,0 +1,71 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestCreateSourceOCI(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		args       string | ||||
| 		assertFunc assertFunc | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "NoArgs", | ||||
| 			args:       "create source oci", | ||||
| 			assertFunc: assertError("name is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "NoURL", | ||||
| 			args:       "create source oci podinfo", | ||||
| 			assertFunc: assertError("url is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "verify provider not specified", | ||||
| 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub", | ||||
| 			assertFunc: assertError("a verification provider must be specified when a secret is specified"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "export manifest", | ||||
| 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export", | ||||
| 			assertFunc: assertGoldenFile("./testdata/oci/export.golden"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "export manifest with secret", | ||||
| 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --secret-ref=creds --export", | ||||
| 			assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "export manifest with verify secret", | ||||
| 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export", | ||||
| 			assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"), | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			cmd := cmdTestCase{ | ||||
| 				args:   tt.args, | ||||
| 				assert: tt.assertFunc, | ||||
| 			} | ||||
| 
 | ||||
| 			cmd.runTestCmd(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,55 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"k8s.io/apimachinery/pkg/util/rand" | ||||
| ) | ||||
| 
 | ||||
| func Test_validateObjectName(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name  string | ||||
| 		valid bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:  "flux-system", | ||||
| 			valid: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "-flux-system", | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "-flux-system-", | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "third.first", | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "THirdfirst", | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "THirdfirst", | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  rand.String(63), | ||||
| 			valid: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  rand.String(64), | ||||
| 			valid: false, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		valid := validateObjectName(tt.name) | ||||
| 		if valid != tt.valid { | ||||
| 			t.Errorf("expected name %q to return %t for validateObjectName func but got %t", | ||||
| 				tt.name, tt.valid, valid) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in New Issue