├── .dockerignore ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── configs │ ├── cr.yaml │ ├── ct-lint-install.yaml │ └── lintconf.yaml └── workflows │ ├── chart-lint-and-test.yml │ ├── chart-publish.yaml │ ├── command-dispatch.yml │ ├── export-repo-secrets.yml │ ├── pull-request.yml │ ├── release.yaml │ ├── run-acceptance-tests.yaml │ ├── sync-images.yaml │ └── weekly-pulumi-update.yml ├── .gitignore ├── .golangci.yml ├── .mise.toml ├── .pulumi.version ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── agent ├── .gitignore ├── Makefile ├── cmd │ ├── init.go │ ├── init_test.go │ ├── mock_test.go │ ├── root.go │ ├── serve.go │ └── version.go ├── hack │ └── token.yaml ├── main.go ├── pkg │ ├── client │ │ ├── credentials.go │ │ └── credentials_test.go │ ├── proto │ │ ├── agent.pb.go │ │ ├── agent.proto │ │ └── agent_grpc.pb.go │ └── server │ │ ├── auth.go │ │ ├── auth_test.go │ │ ├── grpc.go │ │ ├── grpc_test.go │ │ ├── pulumi_errors.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── testdata │ │ ├── foo.txt │ │ ├── hang │ │ └── Pulumi.yaml │ │ ├── simple │ │ ├── Pulumi.nested.yaml │ │ └── Pulumi.yaml │ │ └── uninstallable │ │ └── Pulumi.yaml └── version │ └── version.go ├── artifacthub-repo.yml ├── codecov.yml ├── deploy ├── crds │ ├── auto.pulumi.com_updates.yaml │ ├── auto.pulumi.com_workspaces.yaml │ ├── pulumi.com_programs.yaml │ └── pulumi.com_stacks.yaml ├── deploy-operator-yaml │ └── Pulumi.yaml ├── helm │ └── pulumi-operator │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── README.md.gotmpl │ │ ├── crds │ │ ├── auto.pulumi.com_updates.yaml │ │ ├── auto.pulumi.com_workspaces.yaml │ │ ├── pulumi.com_programs.yaml │ │ └── pulumi.com_stacks.yaml │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── clusterrole.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.yaml │ │ ├── edit_clusterrole.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── role.yaml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ ├── service_account.yaml │ │ ├── servicemonitor.yaml │ │ ├── token_request_role.yaml │ │ ├── token_request_role_binding.yaml │ │ └── view_clusterrole.yaml │ │ └── values.yaml ├── operator_template.yaml └── quickstart │ └── install.yaml ├── docs ├── build.md ├── design │ └── flux-sources.md ├── images │ └── sample-dashboard.png ├── metrics.md ├── migration.md ├── programs.md ├── stacks.md ├── troubleshooting.md ├── updates.md └── workspaces.md ├── examples ├── custom-source │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ └── stack.yaml ├── custom-workspace │ └── workspace.yaml ├── flux-source │ ├── repository.yaml │ └── stack.yaml ├── git-source │ └── stack.yaml ├── program-source │ ├── program.yaml │ └── stack.yaml └── pulumi-ts │ ├── .gitignore │ ├── Pulumi.yaml │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── go.mod ├── go.sum ├── operator ├── .gitignore ├── Makefile ├── PROJECT ├── README.md ├── api │ ├── auto │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── events.go │ │ │ ├── groupversion_info.go │ │ │ ├── update_types.go │ │ │ ├── workspace_types.go │ │ │ └── zz_generated.deepcopy.go │ └── pulumi │ │ ├── shared │ │ ├── doc.go │ │ ├── stack_types.go │ │ └── zz_generated.deepcopy.go │ │ ├── v1 │ │ ├── artifact_types.go │ │ ├── events.go │ │ ├── groupversion_info.go │ │ ├── program_types.go │ │ ├── stack_types.go │ │ └── zz_generated.deepcopy.go │ │ └── v1alpha1 │ │ ├── groupversion_info.go │ │ ├── stack_types.go │ │ └── zz_generated.deepcopy.go ├── cmd │ ├── main.go │ └── main_test.go ├── config │ ├── crd │ │ ├── bases │ │ │ ├── auto.pulumi.com_updates.yaml │ │ │ ├── auto.pulumi.com_workspaces.yaml │ │ │ ├── pulumi.com_programs.yaml │ │ │ └── pulumi.com_stacks.yaml │ │ └── kustomization.yaml │ ├── default │ │ ├── kustomization.yaml │ │ ├── manager_metrics_patch.yaml │ │ └── metrics_service.yaml │ ├── flux │ │ └── network_policy.yaml │ ├── manager │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── quickstart │ │ ├── kustomization.yaml │ │ ├── network_policy.yaml │ │ ├── rbac.yaml │ │ └── service_account.yaml │ ├── rbac │ │ ├── edit.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── metrics_auth_role.yaml │ │ ├── metrics_auth_role_binding.yaml │ │ ├── metrics_reader_role.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ ├── service_account.yaml │ │ ├── token_request_role.yaml │ │ ├── token_request_role_binding.yaml │ │ └── view.yaml │ └── samples │ │ └── kustomization.yaml ├── e2e │ ├── e2e_test.go │ └── testdata │ │ ├── git-auth-nonroot │ │ └── manifests.yaml │ │ ├── issue-801 │ │ ├── manifests.yaml │ │ └── step2 │ │ │ └── manifests.yaml │ │ ├── random-yaml-auth-error │ │ └── manifests.yaml │ │ ├── random-yaml-nonroot │ │ └── manifests.yaml │ │ └── targets │ │ └── manifests.yaml ├── hack │ └── boilerplate.go.txt ├── internal │ ├── apply │ │ ├── auto │ │ │ └── v1alpha1 │ │ │ │ ├── configitem.go │ │ │ │ ├── configvaluefrom.go │ │ │ │ ├── embeddedobjectmeta.go │ │ │ │ ├── embeddedpodtemplatespec.go │ │ │ │ ├── fluxsource.go │ │ │ │ ├── gitauth.go │ │ │ │ ├── gitsource.go │ │ │ │ ├── localsource.go │ │ │ │ ├── update.go │ │ │ │ ├── updatespec.go │ │ │ │ ├── updatestatus.go │ │ │ │ ├── workspace.go │ │ │ │ ├── workspacespec.go │ │ │ │ ├── workspacestack.go │ │ │ │ └── workspacestatus.go │ │ ├── internal │ │ │ └── internal.go │ │ └── utils.go │ ├── controller │ │ ├── auto │ │ │ ├── common.go │ │ │ ├── connect.go │ │ │ ├── mock_update_controller_test.go │ │ │ ├── suite_test.go │ │ │ ├── update_controller.go │ │ │ ├── update_controller_test.go │ │ │ ├── utils.go │ │ │ ├── workspace_controller.go │ │ │ └── workspace_controller_test.go │ │ └── pulumi │ │ │ ├── common.go │ │ │ ├── flux.go │ │ │ ├── flux_test.go │ │ │ ├── git.go │ │ │ ├── git_test.go │ │ │ ├── metrics_program.go │ │ │ ├── metrics_program_test.go │ │ │ ├── metrics_stack.go │ │ │ ├── metrics_stack_test.go │ │ │ ├── migration.go │ │ │ ├── program_controller.go │ │ │ ├── program_controller_test.go │ │ │ ├── session_test.go │ │ │ ├── stack_controller.go │ │ │ ├── stack_controller_test.go │ │ │ ├── suite_test.go │ │ │ └── utils.go │ └── webhook │ │ └── auto │ │ └── v1alpha1 │ │ └── workspace_webhook.go ├── test │ └── crds │ │ └── flux2 │ │ └── v2.3.0 │ │ └── install.yaml └── version │ └── version.go ├── scripts ├── ci-infra-create.sh └── ci-infra-destroy.sh ├── stack-examples └── yaml │ ├── ext_s3_bucket_stack.yaml │ ├── git_auth_with_references.yaml │ ├── nginx_k8s_stack.yaml │ ├── s3_bucket_stack.yaml │ ├── s3_bucket_stack_access_token.yaml │ ├── s3_bucket_stack_basic_auth.yaml │ ├── s3_bucket_stack_ssh.yaml │ └── s3backend │ ├── nginx_k8s_stack.yaml │ └── s3_bucket_stack.yaml └── tools.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | *.md 3 | *.txt 4 | *.out 5 | *.yaml 6 | *.yml 7 | *_test.go 8 | .dockerignore 9 | .gitignore 10 | .git 11 | bin 12 | config 13 | examples 14 | deploy 15 | hack 16 | Dockerfile 17 | Makefile 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Default template 3 | title: '' 4 | labels: needs-triage 5 | 6 | --- 7 | 8 | ### Problem description 9 | 10 | 12 | 13 | - I saw an issue in the following Pulumi Program: 14 | 15 | - I saw an issue in the following documentation: 16 | 17 | - I had trouble finding the information that I needed: 18 | 19 | - Other:
20 | 21 | ### Errors & Logs 22 | 23 | 24 | 25 | ### Affected product version(s) 26 | 27 | 28 | 29 | ### Reproducing the issue 30 | 31 | 32 | 33 | ### Suggestions for a fix 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Proposed changes 8 | 9 | 10 | 11 | ### Related issues (optional) 12 | 13 | 15 | -------------------------------------------------------------------------------- /.github/configs/cr.yaml: -------------------------------------------------------------------------------- 1 | ## Reference: https://github.com/helm/chart-releaser 2 | index-path: "./index.yaml" -------------------------------------------------------------------------------- /.github/configs/ct-lint-install.yaml: -------------------------------------------------------------------------------- 1 | ## Reference: https://github.com/helm/chart-testing/blob/master/doc/ct_lint-and-install.md 2 | # Don't add the 'debug' attribute, otherwise the workflow won't work anymore 3 | # Only Used for the CT Lint Stage 4 | remote: origin 5 | target-branch: master 6 | chart-dirs: 7 | - deploy/helm 8 | helm-extra-args: "--timeout 600s" 9 | validate-chart-schema: false 10 | validate-maintainers: true 11 | validate-yaml: true 12 | exclude-deprecated: true 13 | excluded-charts: [] 14 | all: true 15 | -------------------------------------------------------------------------------- /.github/configs/lintconf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | braces: 4 | min-spaces-inside: 0 5 | max-spaces-inside: 0 6 | min-spaces-inside-empty: -1 7 | max-spaces-inside-empty: -1 8 | brackets: 9 | min-spaces-inside: 0 10 | max-spaces-inside: 0 11 | min-spaces-inside-empty: -1 12 | max-spaces-inside-empty: -1 13 | colons: 14 | max-spaces-before: 0 15 | max-spaces-after: 1 16 | commas: 17 | max-spaces-before: 0 18 | min-spaces-after: 1 19 | max-spaces-after: 1 20 | comments: 21 | require-starting-space: true 22 | min-spaces-from-content: 1 23 | document-end: disable 24 | document-start: disable # No --- to start a file 25 | empty-lines: 26 | max: 2 27 | max-start: 0 28 | max-end: 0 29 | hyphens: 30 | max-spaces-after: 1 31 | indentation: 32 | spaces: consistent 33 | indent-sequences: whatever # - list indentation will handle both indentation and without 34 | check-multi-line-strings: false 35 | key-duplicates: enable 36 | line-length: disable # Lines can be any length 37 | new-line-at-end-of-file: enable 38 | new-lines: 39 | type: unix 40 | trailing-spaces: enable 41 | truthy: 42 | level: warning -------------------------------------------------------------------------------- /.github/workflows/chart-publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: chart-publish 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* # e.g. v2.0.0 7 | 8 | env: 9 | HELM_DOCS_VERSION: "1.14.2" 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | publish: 15 | permissions: 16 | contents: write # for helm/chart-releaser-action to push chart release and create a release 17 | packages: write # for helm/chart-releaser-action to push chart release and create a release 18 | id-token: write # for helm/chart-releaser-action to push chart release and create a release 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install Helm 27 | uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 28 | 29 | - name: Configure Git 30 | run: | 31 | git config user.name "$GITHUB_ACTOR" 32 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 33 | 34 | - name: install helm-docs 35 | run: | 36 | cd /tmp 37 | wget https://github.com/norwoodj/helm-docs/releases/download/v${{env.HELM_DOCS_VERSION}}/helm-docs_${{env.HELM_DOCS_VERSION}}_Linux_x86_64.tar.gz 38 | tar -xvf helm-docs_${{env.HELM_DOCS_VERSION}}_Linux_x86_64.tar.gz 39 | sudo mv helm-docs /usr/local/sbin 40 | 41 | - name: run helm-docs 42 | run: | 43 | helm-docs -t README.md.gotmpl -o README.md -b for-the-badge 44 | 45 | - name: Login to GHCR 46 | uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 47 | with: 48 | registry: ghcr.io 49 | username: ${ GITHUB_REPOSITORY_OWNER } 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Run Artifact Hub lint 53 | run: | 54 | curl -s https://api.github.com/repos/artifacthub/hub/releases/latest | grep -E 'browser_download_url' | grep linux_amd64.tar.gz\" | grep -Eo 'https://[^\"]*' | xargs wget -O - | tar -xz 55 | ./ah lint -p deploy/helm/pulumi-operator || exit 1 56 | rm -f ./ah 57 | 58 | - name: Run chart-releaser 59 | uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 60 | with: 61 | config: "./.github/configs/cr.yaml" 62 | charts_dir: "deploy/helm" 63 | env: 64 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 65 | 66 | - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 67 | - name: Push chart to GHCR 68 | env: 69 | COSIGN_EXPERIMENTAL: 1 70 | run: | 71 | shopt -s nullglob 72 | for pkg in .cr-release-packages/*; do 73 | if [ -z "${pkg:-}" ]; then 74 | break 75 | fi 76 | helm push "${pkg}" oci://ghcr.io/pulumi/helm-charts |& tee .digest 77 | cosign sign -y $(cat .digest | awk -F "[, ]+" '/Pushed/{print $NF}') 78 | done 79 | -------------------------------------------------------------------------------- /.github/workflows/command-dispatch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Command Dispatch for testing 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | jobs: 8 | command-dispatch-for-testing: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: peter-evans/slash-command-dispatch@13bc09769d122a64f75aa5037256f6f2d78be8c4 # v4.0.0 13 | with: 14 | token: ${{ secrets.PULUMI_BOT_TOKEN }} 15 | reaction-token: ${{ secrets.GITHUB_TOKEN }} 16 | commands: run-acceptance-tests 17 | permission: write 18 | issue-type: pull-request 19 | repository: pulumi/pulumi-kubernetes-operator 20 | if: ${{ github.event.issue.pull_request }} -------------------------------------------------------------------------------- /.github/workflows/export-repo-secrets.yml: -------------------------------------------------------------------------------- 1 | permissions: write-all # Equivalent to default permissions plus id-token: write 2 | name: Export secrets to ESC 3 | on: [ workflow_dispatch ] 4 | jobs: 5 | export-to-esc: 6 | runs-on: ubuntu-latest 7 | name: export GitHub secrets to ESC 8 | steps: 9 | - name: Generate a GitHub token 10 | id: generate-token 11 | uses: actions/create-github-app-token@v1 12 | with: 13 | app-id: 1256780 # Export Secrets GitHub App 14 | private-key: ${{ secrets.EXPORT_SECRETS_PRIVATE_KEY }} 15 | - name: Export secrets to ESC 16 | uses: pulumi/esc-export-secrets-action@9d6485759b6adff2538ae91f1b77cc96265c9dad # v1 17 | with: 18 | organization: pulumi 19 | org-environment: github-secrets/pulumi-pulumi-kubernetes-operator 20 | exclude-secrets: EXPORT_SECRETS_PRIVATE_KEY 21 | github-token: ${{ steps.generate-token.outputs.token }} 22 | oidc-auth: true 23 | oidc-requested-token-type: urn:pulumi:token-type:access_token:organization 24 | env: 25 | GITHUB_SECRETS: ${{ toJSON(secrets) }} 26 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pull-request 3 | "on": 4 | pull_request_target: 5 | 6 | env: 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | 9 | jobs: 10 | comment-on-pr: 11 | if: github.event.pull_request.head.repo.full_name != github.repository 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Comment PR 16 | uses: thollander/actions-comment-pull-request@main 17 | with: 18 | message: | 19 | PR is now waiting for a maintainer to run the acceptance tests. This PR will only perform build and linting. 20 | **Note for the maintainer:** To run the acceptance tests, please comment */run-acceptance-tests* on the PR 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pulumi Kubernetes Operator Release 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* # e.g. v2.0.0 7 | - v*.*-*.* # e.g. v2.0-beta.0 8 | - v*.*.*-*.* # e.g. v2.0.0-beta.1 9 | env: 10 | PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | VERSION: ${{ github.ref_name }} 13 | permissions: 14 | contents: write 15 | jobs: 16 | docker: 17 | name: Build & Push Docker Images 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Login to Docker Hub 21 | uses: docker/login-action@v3 22 | with: 23 | username: ${{ secrets.DOCKER_USERNAME }} 24 | password: ${{ secrets.DOCKER_PASSWORD }} 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | - name: Build and push 30 | uses: docker/build-push-action@v6 31 | with: 32 | push: true 33 | platforms: linux/amd64,linux/arm64 34 | tags: | 35 | pulumi/pulumi-kubernetes-operator:${{ env.VERSION }} 36 | build-args: | 37 | VERSION=${{ env.VERSION }} 38 | # sync-images: 39 | # uses: ./.github/workflows/sync-images.yaml 40 | # needs: docker 41 | # secrets: inherit 42 | # with: 43 | # operator_version: ${{ github.ref_name }} 44 | release: 45 | name: Create a GitHub Release 46 | needs: [docker] 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | - name: Create a GH release 52 | uses: softprops/action-gh-release@v2 53 | with: 54 | name: pulumi-kubernetes-operator-${{ env.VERSION }} 55 | prerelease: ${{ contains(env.VERSION, '-') }} 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/sync-images.yaml: -------------------------------------------------------------------------------- 1 | # Copies all Pulumi Kubernetes Operator OCI images for the supplied version from Docker Hub to 2 | # AWS ECR Public Gallery and GitHub Container Registry. 3 | name: Sync Docker Hub Images to ECR and GHCR 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | operator_version: 8 | description: The image tag to copy, fully specified, e.g. "v2.0.0" 9 | type: string 10 | required: true 11 | workflow_call: 12 | inputs: 13 | operator_version: 14 | description: The image tag to copy, fully specified, e.g. "v2.0.0" 15 | type: string 16 | required: true 17 | repository_dispatch: 18 | types: 19 | - sync-ecr 20 | 21 | env: 22 | DOCKER_USERNAME: pulumi 23 | OPERATOR_VERSION: ${{ inputs.operator_version || github.event.client_payload.ref }} 24 | OPERATOR_IMAGE_NAME: pulumi-kubernetes-operator 25 | 26 | jobs: 27 | sync-to-ecr: 28 | name: Pulumi Kubernetes Operator image 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Configure AWS Credentials 32 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4 33 | with: 34 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 35 | aws-region: us-east-2 36 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 37 | role-duration-seconds: 3600 38 | role-external-id: upload-pulumi-release 39 | role-session-name: pulumi@githubActions 40 | role-to-assume: ${{ secrets.AWS_UPLOAD_ROLE_ARN }} 41 | - name: Login to GitHub Container Registry 42 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | - name: Get Public ECR Authorization token 48 | run: | 49 | aws --region us-east-1 ecr-public get-authorization-token \ 50 | --query 'authorizationData.authorizationToken' | \ 51 | tr -d '"' | base64 --decode | cut -d: -f2 | \ 52 | docker login -u AWS --password-stdin https://public.ecr.aws 53 | - name: Copy ${{ env.OPERATOR_VERSION }} image to AWS Public ECR 54 | run: | 55 | skopeo copy --all docker://docker.io/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }} docker://public.ecr.aws/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }} 56 | - name: Copy ${{ env.OPERATOR_VERSION }} image to GitHub Container Registry 57 | run: | 58 | skopeo copy --all docker://docker.io/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }} docker://ghcr.io/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }} 59 | - name: Output build summary 60 | run: | 61 | SUMMARY=$'# Image Syncing Summary\nSource Image: `pulumi/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }}`\n\nDestination Images:\n- `public.ecr.aws/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }}`\n- `ghcr.io/${{ env.DOCKER_USERNAME }}/${{ env.OPERATOR_IMAGE_NAME }}:${{ env.OPERATOR_VERSION }}`' 62 | echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /.github/workflows/weekly-pulumi-update.yml: -------------------------------------------------------------------------------- 1 | name: weekly-pulumi-update 2 | "on": 3 | schedule: 4 | - cron: 35 12 * * 4 5 | workflow_dispatch: {} 6 | 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.PULUMI_BOT_TOKEN }} 9 | 10 | jobs: 11 | update-go-mod: 12 | name: Update Go mods 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | goversion: [1.24.x] 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v4 20 | - name: Unshallow clone for tags 21 | run: git fetch --prune --unshallow --tags 22 | - name: Install Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.goversion }} 26 | - name: Install pulumictl 27 | uses: jaxxstorm/action-install-gh-release@v1.1.0 28 | with: 29 | repo: pulumi/pulumictl 30 | - name: Install Pulumi CLI 31 | uses: pulumi/actions@df5a93ad715135263c732ba288301bd044c383c0 # v6.3.0 32 | with: 33 | pulumi-version-file: .pulumi.version 34 | - name: Preparing Git Branch 35 | run: | 36 | git config --local user.email "bot@pulumi.com" 37 | git config --local user.name "pulumi-bot" 38 | git checkout -b update-pulumi/${{ github.run_id }}-${{ github.run_number }} 39 | - name: Update pulumi/pulumi 40 | id: gomod 41 | run: | 42 | go get github.com/pulumi/pulumi/sdk/v3 43 | go mod tidy 44 | git update-index -q --refresh 45 | if ! git diff-files --quiet; then 46 | echo changes=1 >> "$GITHUB_OUTPUT" 47 | fi 48 | - name: Build codegen + Schema + SDKs 49 | if: steps.gomod.outputs.changes != 0 50 | run: make build 51 | - name: Commit changes 52 | if: steps.gomod.outputs.changes != 0 53 | run: | 54 | git add . 55 | git commit -m "Updated modules" 56 | git push origin update-pulumi/${{ github.run_id }}-${{ github.run_number }} 57 | - name: Open a pull request 58 | if: steps.gomod.outputs.changes != 0 59 | uses: repo-sync/pull-request@v2 60 | with: 61 | source_branch: "update-pulumi/${{ github.run_id }}-${{ github.run_number }}" 62 | destination_branch: "master" 63 | pr_title: "Automated pulumi/pulumi upgrade" 64 | pr_label: "automation/merge" 65 | pr_allow_empty: true 66 | github_token: ${{ secrets.PULUMI_BOT_TOKEN }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### makefile ### 2 | pulumi-kubernetes-operator 3 | # Temporary Build Files 4 | build/_output 5 | build/_test 6 | .pulumi 7 | Pulumi.*.yaml 8 | 9 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 10 | ### Emacs ### 11 | # -*- mode: gitignore; -*- 12 | *~ 13 | \#*\# 14 | .DS_Store 15 | /.emacs.desktop 16 | /.emacs.desktop.lock 17 | *.elc 18 | auto-save-list 19 | tramp 20 | .\#* 21 | # Org-mode 22 | .org-id-locations 23 | *_archive 24 | # flymake-mode 25 | *_flymake.* 26 | # eshell files 27 | /eshell/history 28 | /eshell/lastdir 29 | # elpa packages 30 | /elpa/ 31 | # reftex files 32 | *.rel 33 | # AUCTeX auto folder 34 | /auto/ 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | # Flycheck 39 | flycheck_*.el 40 | # server auth directory 41 | /server/ 42 | # projectiles files 43 | .projectile 44 | projectile-bookmarks.eld 45 | # directory configuration 46 | .dir-locals.el 47 | # saveplace 48 | places 49 | # url cache 50 | url/cache/ 51 | # cedet 52 | ede-projects.el 53 | # smex 54 | smex-items 55 | # company-statistics 56 | company-statistics-cache.el 57 | # anaconda-mode 58 | anaconda-mode/ 59 | ### Go ### 60 | # Binaries for programs and plugins 61 | *.exe 62 | *.exe~ 63 | *.dll 64 | *.so 65 | *.dylib 66 | # Test binary, build with 'go test -c' 67 | *.test 68 | # Output of the go coverage tool, specifically when used with LiteIDE 69 | *.out 70 | ### Vim ### 71 | # swap 72 | .sw[a-p] 73 | .*.sw[a-p] 74 | # session 75 | Session.vim 76 | # temporary 77 | .netrwhist 78 | # auto-generated tag files 79 | tags 80 | ### VisualStudioCode ### 81 | .vscode/* 82 | !.vscode/settings.json 83 | !.vscode/tasks.json 84 | !.vscode/launch.json 85 | !.vscode/extensions.json 86 | !.vscode/*.code-snippets 87 | # Local History for Visual Studio Code 88 | .history/ 89 | # Built Visual Studio Code Extensions 90 | *.vsix 91 | ### IntelliJ ### 92 | .idea 93 | .envrc 94 | bin/* 95 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | allow-parallel-runners: true 4 | modules-download-mode: readonly 5 | 6 | linters: 7 | disable-all: true 8 | enable: 9 | - depguard 10 | - goconst 11 | - gofmt 12 | - staticcheck 13 | - goheader 14 | - gosec 15 | - govet 16 | - ineffassign 17 | - misspell 18 | - nakedret 19 | - nolintlint 20 | - unconvert 21 | - gosimple 22 | 23 | linters-settings: 24 | nolintlint: 25 | # Some linter exclusions are added to generated or templated files 26 | # pre-emptively. 27 | # Don't complain about these. 28 | allow-unused: true 29 | govet: 30 | enable: 31 | - nilness 32 | # Reject comparisons of reflect.Value with DeepEqual or '=='. 33 | - reflectvaluecompare 34 | # Reject sort.Slice calls with a non-slice argument. 35 | - sortslice 36 | # Detect write to struct/arrays by-value that aren't read again. 37 | - unusedwrite 38 | depguard: 39 | rules: 40 | protobuf: 41 | deny: 42 | - pkg: "github.com/golang/protobuf" 43 | desc: Use google.golang.org/protobuf instead 44 | goheader: 45 | values: 46 | regexp: 47 | COPYRIGHT_YEARS: (\d{4}-)?\d{4} 48 | WHITESPACE: \s* 49 | template: |- 50 | Copyright {{ COPYRIGHT_YEARS }}, Pulumi Corporation. 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | {{ WHITESPACE }}http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | 64 | issues: 65 | # don't skip warning about doc comments 66 | # don't exclude the default set of lint 67 | exclude-use-default: false 68 | # restore some of the defaults 69 | # (fill in the rest as needed) 70 | exclude-rules: 71 | - linters: [goconst] 72 | text: "string `test/pulumi:v3.42.0` has 3 occurrences, make it a constant" 73 | 74 | - path: "api/*" 75 | linters: 76 | - lll 77 | - path: "internal/*" 78 | linters: 79 | - dupl 80 | - lll 81 | # Ignore gosec warning about shelling out to make/kind/kubectl. 82 | - path: "e2e/*" 83 | linters: [gosec] 84 | text: "G204:" -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | golangci-lint = "1.58.1" 3 | etcd = "3.5.16" 4 | protoc = "29.3" 5 | protoc-gen-go = "1.36.4" 6 | protoc-gen-go-grpc = "1.5.1" 7 | kubebuilder = "4.2.0" 8 | envsubst = "1.4.2" 9 | go = "1.24" 10 | 11 | [settings] 12 | experimental = true # Enable Go backend. 13 | -------------------------------------------------------------------------------- /.pulumi.version: -------------------------------------------------------------------------------- 1 | 3.147.0 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "golang.go", 4 | "ms-kubernetes-tools.vscode-kubernetes-tools" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Manager", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "operator/cmd", 13 | "args": [ 14 | "--zap-devel", 15 | ], 16 | "env": { 17 | "WORKSPACE_LOCALHOST": "localhost:50051", 18 | "SOURCE_CONTROLLER_LOCALHOST": "localhost:9090", 19 | }, 20 | }, 21 | { 22 | "name": "Agent", 23 | "type": "go", 24 | "request": "launch", 25 | "mode": "auto", 26 | "program": "agent", 27 | "args": [ 28 | "serve", 29 | "-v=false", 30 | "--workspace=${input:workdir}", 31 | "-s=dev" 32 | ] 33 | }, 34 | { 35 | "name": "Agent (kubernetes)", 36 | "type": "go", 37 | "request": "launch", 38 | "mode": "auto", 39 | "program": "agent", 40 | "args": [ 41 | "serve", 42 | "-v=true", 43 | "--workspace=${input:workdir}", 44 | "-s=dev", 45 | "--auth-mode=kube", 46 | "--kube-workspace-namespace=default", 47 | "--kube-workspace-name=random-yaml" 48 | ], 49 | "env": { 50 | "POD_NAMESPACE": "default", 51 | "POD_SA_NAME": "fake" 52 | } 53 | } 54 | ], 55 | "inputs": [ 56 | { 57 | "id": "workdir", 58 | "description": "Please provide the Pulumi program directory", 59 | "type": "promptString" 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "label": "make: build", 7 | "command": "make build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "problemMatcher": [ 13 | "$go" 14 | ] 15 | }, 16 | { 17 | "type": "shell", 18 | "label": "make: test", 19 | "command": "make test", 20 | "group": { 21 | "kind": "test", 22 | "isDefault": true 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build a base image with modules cached. 2 | FROM --platform=${BUILDPLATFORM} golang:1.24 AS base 3 | ARG TARGETARCH 4 | 5 | # Install tini to reap zombie processes. 6 | ENV TINI_VERSION=v0.19.0 7 | ADD --chmod=755 https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TARGETARCH} /tini 8 | 9 | COPY /go.mod go.mod 10 | COPY /go.sum go.sum 11 | 12 | ENV GOCACHE=/root/.cache/go-build 13 | ENV GOMODCACHE=/go/pkg/mod 14 | RUN --mount=type=cache,target=${GOMODCACHE} \ 15 | go mod download 16 | 17 | # Build the operator. 18 | FROM --platform=${BUILDPLATFORM} base AS op-builder 19 | ARG TARGETOS 20 | ARG TARGETARCH 21 | 22 | ARG VERSION 23 | RUN --mount=type=cache,target=${GOCACHE} \ 24 | --mount=type=cache,target=${GOMODCACHE} \ 25 | --mount=type=bind,source=/agent,target=./agent \ 26 | --mount=type=bind,source=/operator,target=./operator \ 27 | CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ 28 | go build -a -o /manager -ldflags="-w -X github.com/pulumi/pulumi-kubernetes-operator/v2/operator/version.Version=${VERSION}" ./operator/cmd/main.go 29 | 30 | # Build the agent. 31 | FROM --platform=${BUILDPLATFORM} base AS agent-builder 32 | ARG TARGETOS 33 | ARG TARGETARCH 34 | 35 | ARG VERSION 36 | RUN --mount=type=cache,target=${GOCACHE} \ 37 | --mount=type=cache,target=${GOMODCACHE} \ 38 | --mount=type=bind,source=/agent,target=./agent \ 39 | CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ 40 | go build -a -o /agent -ldflags "-w -X github.com/pulumi/pulumi-kubernetes-operator/v2/agent/version.Version=${VERSION}" ./agent/main.go 41 | 42 | # Use distroless as minimal base image to package the manager binary 43 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 44 | FROM gcr.io/distroless/static-debian12:debug-nonroot 45 | 46 | COPY --from=base /tini /tini 47 | COPY --from=op-builder /manager /manager 48 | COPY --from=agent-builder /agent /agent 49 | USER 65532:65532 50 | 51 | ENTRYPOINT ["/tini", "--"] 52 | CMD ["/manager"] 53 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | pulumi-kubernetes-agent 2 | coverage.out -------------------------------------------------------------------------------- /agent/Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= $(shell git describe --tags --always --dirty) 2 | 3 | all: agent 4 | 5 | protoc: 6 | cd pkg/proto && protoc --go_out=. --go_opt=paths=source_relative \ 7 | --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto 8 | 9 | test: 10 | go test -covermode=atomic -coverprofile=coverage.out -coverpkg=./... -v ./... 11 | 12 | agent: 13 | go build -o pulumi-kubernetes-agent \ 14 | -ldflags "-X github.com/pulumi/pulumi-kubernetes-operator/v2/agent/version.Version=${VERSION}" \ 15 | main.go 16 | 17 | GOLANGCI_LINT = $(shell pwd)/../bin/golangci-lint 18 | 19 | .PHONY: lint 20 | lint: ## Run golangci-lint linter & yamllint 21 | $(GOLANGCI_LINT) run --config ../.golangci.yml 22 | 23 | .PHONY: lint-fix 24 | lint-fix: ## Run golangci-lint linter and perform fixes 25 | $(GOLANGCI_LINT) run --fix --config ../.golangci.yml 26 | 27 | .PHONY: all agent protoc test 28 | 29 | -------------------------------------------------------------------------------- /agent/cmd/init_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/go-git/go-git/v5" 21 | "github.com/spf13/cobra" 22 | "github.com/stretchr/testify/assert" 23 | "go.uber.org/mock/gomock" 24 | "go.uber.org/zap" 25 | ) 26 | 27 | func TestInitFluxSource(t *testing.T) { 28 | t.Parallel() 29 | dir := t.TempDir() 30 | log := zap.L().Named(t.Name()).Sugar() 31 | 32 | url := "https://github.com/pulumi/examples.git" 33 | digest := "sha256:bcbed45526b241ab3366707b5a58c900e9d60a1d5c385cdfe976b1306584b454" 34 | 35 | ctrl := gomock.NewController(t) 36 | f := NewMockfetchWithContexter(ctrl) 37 | f.EXPECT().URL().Return(url).AnyTimes() 38 | f.EXPECT().Digest().Return(digest).AnyTimes() 39 | f.EXPECT().FetchWithContext(gomock.Any(), url, digest, dir).Return(nil) 40 | 41 | err := runInit(t.Context(), log, dir, f, nil) 42 | assert.NoError(t, err) 43 | } 44 | 45 | func TestInitGitSource(t *testing.T) { 46 | t.Parallel() 47 | dir := t.TempDir() 48 | log := zap.L().Named(t.Name()).Sugar() 49 | 50 | ctrl := gomock.NewController(t) 51 | g := NewMocknewLocalWorkspacer(ctrl) 52 | g.EXPECT().URL().Return("https://github.com/pulumi/examples.git").AnyTimes() 53 | g.EXPECT().Revision().Return("f143bd369afcb5455edb54c2b90ad7aaac719339").AnyTimes() 54 | 55 | // Simulate a successful pull, followed by a second unnecessary pull. 56 | // TODO: Check auth etc. 57 | gomock.InOrder( 58 | g.EXPECT().NewLocalWorkspace(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil), 59 | g.EXPECT().NewLocalWorkspace(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, git.ErrRepositoryAlreadyExists), 60 | ) 61 | 62 | err := runInit(t.Context(), log, dir, nil, g) 63 | assert.NoError(t, err) 64 | 65 | err = runInit(t.Context(), log, dir, nil, g) 66 | assert.NoError(t, err) 67 | } 68 | 69 | func TestInitGitSourceE2E(t *testing.T) { 70 | if testing.Short() { 71 | t.Skip() 72 | } 73 | t.Parallel() 74 | 75 | // Copy the command so we don't mutate it. 76 | root := cobra.Command(*rootCmd) //nolint:unconvert // We want to copy. 77 | root.SetArgs([]string{ 78 | "init", 79 | "--git-url=https://github.com/git-fixtures/basic", 80 | "--git-revision=6ecf0ef2c2dffb796033e5a02219af86ec6584e5", 81 | "--target-dir=" + t.TempDir(), 82 | }) 83 | assert.NoError(t, root.Execute()) 84 | } 85 | -------------------------------------------------------------------------------- /agent/cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "os" 19 | 20 | "flag" 21 | 22 | "github.com/spf13/cobra" 23 | "go.uber.org/zap" 24 | "k8s.io/client-go/rest" 25 | "sigs.k8s.io/controller-runtime/pkg/client/config" 26 | ) 27 | 28 | var ( 29 | verbose bool 30 | kubeContext string 31 | ) 32 | 33 | // a command-specific logger 34 | var log *zap.SugaredLogger 35 | 36 | // rootCmd represents the base command when called without any subcommands 37 | var rootCmd = &cobra.Command{ 38 | Use: "agent", 39 | Short: "Pulumi Kubernetes Operator Agent", 40 | Long: `Provides tooling and a gRPC service for the Pulumi Kubernetes Operator 41 | to use to perform stack operations.`, 42 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 43 | var err error 44 | 45 | // initialize the global logger 46 | zc := zap.NewDevelopmentConfig() 47 | zc.DisableCaller = true 48 | zc.DisableStacktrace = true 49 | if !verbose { 50 | zc.Level.SetLevel(zap.InfoLevel) 51 | } 52 | zapLog, err := zc.Build() 53 | if err != nil { 54 | return err 55 | } 56 | zap.ReplaceGlobals(zapLog) 57 | 58 | // initialize a command-specific logger 59 | log = zap.L().Named("cmd").Named(cmd.Name()).Sugar() 60 | cmd.SilenceErrors = true 61 | return nil 62 | }, 63 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 64 | // ignore sync errors: https://github.com/uber-go/zap/pull/347 65 | _ = zap.L().Sync() 66 | }, 67 | } 68 | 69 | // Execute adds all child commands to the root command and sets flags appropriately. 70 | // This is called by main.main(). It only needs to happen once to the rootCmd. 71 | func Execute() { 72 | err := rootCmd.Execute() 73 | if err != nil { 74 | if log != nil { 75 | log.Error(err.Error()) 76 | } 77 | os.Exit(1) 78 | } 79 | } 80 | 81 | func init() { 82 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") 83 | 84 | // register the Kubernetes flags (e.g. for serve command when using Kubernetes RBAC for authorization) 85 | fs := flag.NewFlagSet("kubernetes", flag.ExitOnError) 86 | config.RegisterFlags(fs) 87 | rootCmd.PersistentFlags().AddGoFlagSet(fs) 88 | rootCmd.PersistentFlags().StringVar(&kubeContext, "context", "", "Kubernetes context override") 89 | } 90 | 91 | func GetKubeConfig() (*rest.Config, error) { 92 | return config.GetConfigWithContext(kubeContext) 93 | } 94 | -------------------------------------------------------------------------------- /agent/cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/version" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // versionCmd represents the version command 23 | var versionCmd = &cobra.Command{ 24 | Use: "version", 25 | Short: "Print version information", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | log.Infow("Pulumi Kubernetes Agent", "version", version.Version) 28 | }, 29 | } 30 | 31 | func init() { 32 | rootCmd.AddCommand(versionCmd) 33 | } 34 | -------------------------------------------------------------------------------- /agent/hack/token.yaml: -------------------------------------------------------------------------------- 1 | # Apply this manifest file to create a token with which to authenticate to the agent. 2 | # To get the token, run the following command: kubectl describe secret/dev-token 3 | # To test: kubectl auth can-i use workspaces/random-yaml --subresource rpc --as system:serviceaccount:default:dev 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: dev 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: dev-token 13 | annotations: 14 | kubernetes.io/service-account.name: dev 15 | type: kubernetes.io/service-account-token 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: RoleBinding 19 | metadata: 20 | name: dev:cluster-admin 21 | roleRef: 22 | apiGroup: rbac.authorization.k8s.io 23 | kind: ClusterRole 24 | name: cluster-admin 25 | subjects: 26 | - kind: ServiceAccount 27 | name: dev -------------------------------------------------------------------------------- /agent/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /agent/pkg/server/grpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "context" 19 | "net" 20 | 21 | grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 22 | grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" 23 | grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" 24 | pb "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/pkg/proto" 25 | "go.uber.org/zap" 26 | "google.golang.org/grpc" 27 | ) 28 | 29 | // GRPC serves the automation service. 30 | type GRPC struct { 31 | *grpc.Server 32 | wrapped *Server 33 | log *zap.SugaredLogger 34 | } 35 | 36 | // NewGRPC constructs a new gRPC server with logging and authentication support. 37 | func NewGRPC(rootLogger *zap.SugaredLogger, server *Server, authF grpc_auth.AuthFunc) *GRPC { 38 | log := rootLogger.Named("grpc") 39 | // Configure the grpc server. 40 | // Apply zap logging and use filters to reduce log verbosity as needed. 41 | serverOpts := []grpc_zap.Option{ 42 | grpc_zap.WithDecider(func(fullMethodName string, err error) bool { 43 | return true 44 | }), 45 | } 46 | grpc_zap.ReplaceGrpcLoggerV2WithVerbosity(log.Desugar(), int(log.Level())) 47 | 48 | // Apply a default authentication function. 49 | if authF == nil { 50 | authF = func(ctx context.Context) (context.Context, error) { 51 | return ctx, nil 52 | } 53 | } 54 | 55 | // Create the gRPC server. 56 | s := grpc.NewServer( 57 | grpc.ChainUnaryInterceptor( 58 | grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), 59 | grpc_zap.UnaryServerInterceptor(log.Desugar(), serverOpts...), 60 | grpc_auth.UnaryServerInterceptor(authF), 61 | ), 62 | grpc.ChainStreamInterceptor( 63 | grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), 64 | grpc_zap.StreamServerInterceptor(log.Desugar(), serverOpts...), 65 | grpc_auth.StreamServerInterceptor(authF), 66 | ), 67 | ) 68 | pb.RegisterAutomationServiceServer(s, server) 69 | 70 | return &GRPC{Server: s, wrapped: server, log: log} 71 | } 72 | 73 | // Serve wraps the underlying gRPC server with graceful shutdown. When the 74 | // given context is canceled a SIGTERM is propagated to all child processes 75 | // (spawned by Automation API) and requests are given an opportunity to exit 76 | // cleanly. 77 | func (s *GRPC) Serve(ctx context.Context, l net.Listener) error { 78 | go func() { 79 | <-ctx.Done() 80 | s.log.Infow("shutting down the server") 81 | s.wrapped.Cancel() // Non-blocking. 82 | s.GracefulStop() // Blocks until outstanding requests have finished. 83 | }() 84 | 85 | return s.Server.Serve(l) 86 | } 87 | -------------------------------------------------------------------------------- /agent/pkg/server/grpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "context" 19 | "io" 20 | "net" 21 | "testing" 22 | "time" 23 | 24 | pb "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/pkg/proto" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | "go.uber.org/zap" 28 | "google.golang.org/grpc" 29 | "google.golang.org/grpc/credentials/insecure" 30 | "google.golang.org/grpc/test/bufconn" 31 | ) 32 | 33 | //nolint:paralleltest // Kills child subprocesses. 34 | func TestGracefulShutdown(t *testing.T) { 35 | // Give this test 10 seconds to spin up and shut down gracefully. Should be 36 | // more than enough -- typically takes ~2s. 37 | ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) 38 | defer cancel() 39 | 40 | // Setup the server using an in-memory listener. 41 | tc := newTC(ctx, t, tcOptions{ProjectDir: "./testdata/hang", Stacks: []string{"test"}}) 42 | log := zap.L().Named("test").Sugar() 43 | lis := bufconn.Listen(1024) 44 | s := NewGRPC(log, tc.server, nil) 45 | go func() { 46 | // This should exit cleanly if we shut down gracefully. 47 | if err := s.Serve(ctx, lis); err != nil { 48 | t.Errorf("unexpected serve error: %s", err) 49 | } 50 | }() 51 | 52 | // Setup the client. 53 | conn, err := grpc.NewClient("passthrough://bufconn", 54 | grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { 55 | return lis.Dial() 56 | }), 57 | grpc.WithTransportCredentials(insecure.NewCredentials()), 58 | ) 59 | require.NoError(t, err) 60 | client := pb.NewAutomationServiceClient(conn) 61 | 62 | // Initiate an update using a context separate from the server's. 63 | stream, err := client.Up(t.Context(), &pb.UpRequest{}) 64 | require.NoError(t, err) 65 | 66 | // Stream events from our update. We will cause the server to shut down 67 | // once a resourcePreEvent is observed, and it should continue to send 68 | // events as it shuts down. If it exits cleanly, we expect it to return a 69 | // cancelEvent along with an non-nil error summarizing what was updated. 70 | sawCancelEvent := false 71 | sawSummary := false 72 | for { 73 | msg, err := stream.Recv() 74 | if err == io.EOF { 75 | break 76 | } 77 | // A non-nil error is expected if the server acked our cancellation. 78 | // This includes a final summary of any changes applied before the 79 | // update was canceled. 80 | if err != nil && sawCancelEvent { 81 | assert.ErrorContains(t, err, "urn:pulumi:test::hang::pulumi:pulumi:Stack::hang-test") 82 | assert.ErrorContains(t, err, "error: update canceled") 83 | sawSummary = true 84 | break 85 | } 86 | require.NoError(t, err) 87 | 88 | // Start shutting down our server once we see a resourcePreEvent. 89 | // Ideally this would invoke our signal handler but this is close 90 | // enough. 91 | if _, ok := msg.GetEvent().AsMap()["resourcePreEvent"]; ok { 92 | cancel() 93 | continue 94 | } 95 | 96 | // We should eventually see an ack for the cancellation. 97 | if _, ok := msg.GetEvent().AsMap()["cancelEvent"]; ok { 98 | sawCancelEvent = true 99 | } 100 | } 101 | 102 | assert.True(t, sawCancelEvent) 103 | assert.True(t, sawSummary) 104 | } 105 | -------------------------------------------------------------------------------- /agent/pkg/server/pulumi_errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "strings" 19 | 20 | pb "github.com/pulumi/pulumi-kubernetes-operator/v2/agent/pkg/proto" 21 | "google.golang.org/grpc/status" 22 | ) 23 | 24 | type knownPulumiErrors map[string]*pb.PulumiErrorInfo 25 | 26 | // knownErrors is a map where the keys are string snippets of the CLI error message, 27 | // and the values are the structured error we want to return to the client. 28 | var knownErrors = knownPulumiErrors{ 29 | "Conflict: Another update is currently in progress": { 30 | Message: "Another update is currently in progress", 31 | Reason: "UpdateConflict", 32 | Code: 409, 33 | }, 34 | "invalid access token": { 35 | Message: "Invalid access token used to authenticate with Pulumi Cloud", 36 | Reason: "InvalidAccessToken", 37 | Code: 401, 38 | }, 39 | } 40 | 41 | // withPulumiErrorInfo iterates over known errors and checks if the provided error matches any of them. 42 | // If it does, it appends the structured error to the status. 43 | func withPulumiErrorInfo(st *status.Status, err error) *status.Status { 44 | if err == nil { 45 | return st 46 | } 47 | 48 | for errString, structuredErr := range knownErrors { 49 | if !strings.Contains(err.Error(), errString) { 50 | continue 51 | } 52 | 53 | ds, err := st.WithDetails(structuredErr) 54 | if err != nil { 55 | // If we can't add the structured error, return the original status. 56 | return st 57 | } 58 | return ds 59 | } 60 | return st 61 | } 62 | -------------------------------------------------------------------------------- /agent/pkg/server/testdata/foo.txt: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /agent/pkg/server/testdata/hang/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: hang 2 | runtime: yaml 3 | description: A program that takes a very long time in order to test termination behavior. 4 | 5 | resources: 6 | sleep: 7 | type: time:Sleep 8 | properties: 9 | createDuration: 300s 10 | -------------------------------------------------------------------------------- /agent/pkg/server/testdata/simple/Pulumi.nested.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | aws:region: us-west-2 3 | simple:bar: 1234 4 | simple:foo: aha 5 | simple:myList: 6 | - one 7 | - two 8 | - three 9 | simple:outer: 10 | inner: my_value 11 | other: something_else 12 | -------------------------------------------------------------------------------- /agent/pkg/server/testdata/simple/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: simple 2 | runtime: yaml 3 | -------------------------------------------------------------------------------- /agent/pkg/server/testdata/uninstallable/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: uninstallable 2 | runtime: go 3 | description: A project that cannot be installed because it lacks a go.mod 4 | -------------------------------------------------------------------------------- /agent/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | var Version string = "v2.1.0" 18 | -------------------------------------------------------------------------------- /artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # from https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml 2 | repositoryID: 8fcdad40-fcf5-46af-a185-5ccbfa972b03 3 | owners: 4 | - name: Engin Diri 5 | email: engin.diri@ediri.de 6 | - name: Eron Wright 7 | email: eron@pulumi.com 8 | - name: Ramon Quitales 9 | email: ramon@pulumi.com -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | ignore: 10 | - "**/zz_generated.deepcopy.go" 11 | - "**/*.pb.go" 12 | - "operator/internal/apply/**/*.go" 13 | -------------------------------------------------------------------------------- /deploy/deploy-operator-yaml/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: pulumi-operator-yaml 2 | runtime: yaml 3 | description: | 4 | Deploys the Pulumi Kubernetes Operator. 5 | config: 6 | version: # The version to install of the Pulumi Kubernetes Operator. 7 | type: string 8 | default: v2.1.0 9 | resources: 10 | pko: 11 | type: kubernetes:kustomize/v2:Directory 12 | properties: 13 | directory: https://github.com/pulumi/pulumi-kubernetes-operator//operator/config/default/?ref=${version} -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: pulumi-kubernetes-operator 3 | description: A Helm chart for the Pulumi Kubernetes Operator 4 | home: https://pulumi.com 5 | sources: 6 | - https://github.com/pulumi/pulumi-kubernetes-operator 7 | 8 | icon: https://www.pulumi.com/logos/brand/avatar-on-white.svg 9 | 10 | type: application 11 | 12 | version: "2.1.0" 13 | appVersion: "v2.1.0" 14 | 15 | keywords: 16 | - pulumi 17 | - kubernetes 18 | - operator 19 | 20 | maintainers: 21 | - name: Bryce Lampe 22 | email: bryce@pulumi.com 23 | - name: dirien 24 | email: engin@pulumi.com 25 | - name: Eron Wright 26 | email: eron@pulumi.com 27 | 28 | annotations: 29 | artifacthub.io/prerelease: "false" 30 | artifacthub.io/containsSecurityUpdates: "false" 31 | artifacthub.io/changes: | 32 | - Configure the agent image (#919) 33 | - More cloud logging options (#927) 34 | artifacthub.io/images: | 35 | - name: pulumi-kubernetes-operator 36 | image: docker.io/pulumi/pulumi-kubernetes-operator:v2.1.0 37 | platforms: 38 | - linux/amd64 39 | - linux/arm64 40 | artifacthub.io/license: Apache-2.0 41 | artifacthub.io/links: | 42 | - name: website 43 | url: https://pulumi.com 44 | artifacthub.io/maintainers: | 45 | - name: Bryce Lampe 46 | email: bryce@pulumi.com 47 | - name: dirien 48 | email: engin@pulumi.com 49 | - name: Eron Wright 50 | email: eron@pulumi.com -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | # Pulumi Kubernetes Operator - Helm Chart 2 | 3 | {{ template "chart.badgesSection" . }} 4 | 5 | ## Description 📜 6 | 7 | {{ template "chart.description" . }} 8 | 9 | ## Usage (via OCI Registry) 10 | 11 | To install the chart using the OCI artifact, run: 12 | 13 | ```bash 14 | helm install --create-namespace -n pulumi-kubernetes-operator pulumi-kubernetes-operator \ 15 | oci://ghcr.io/pulumi/helm-charts/pulumi-kubernetes-operator --version {{ .Version }} 16 | ``` 17 | 18 | After a few seconds, the `pulumi-kubernetes-operator` release should be deployed and running. 19 | 20 | > **Tip**: List all releases using `helm list`, a release is a name used to track a specific deployment 21 | 22 | ### Uninstalling the Chart 🗑️ 23 | 24 | To uninstall the `pulumi-kubernetes-operator` deployment: 25 | 26 | ```bash 27 | helm uninstall -n pulumi-kubernetes-operator pulumi-kubernetes-operator 28 | ``` 29 | 30 | The command removes all the Kubernetes components associated with the chart and deletes the release. 31 | 32 | {{ template "chart.valuesSection" . }} 33 | 34 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, 35 | 36 | ```bash 37 | helm install --create-namespace -n pulumi-kubernetes-operator pulumi-kubernetes-operator \ 38 | oci://ghcr.io/pulumi/helm-charts/pulumi-kubernetes-operator --set image.tag=latest 39 | ``` 40 | 41 | Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, 42 | 43 | ```bash 44 | helm install --create-namespace -n pulumi-kubernetes-operator pulumi-kubernetes-operator \ 45 | oci://ghcr.io/pulumi/helm-charts/pulumi-kubernetes-operator -f values.yaml 46 | ``` 47 | 48 | > **Tip**: You can use the default [values.yaml](values.yaml) 49 | 50 | ## Contributing 🤝 51 | 52 | ### Contributing via GitHub 53 | 54 | Feel free to join. Checkout the [contributing guide](CONTRIBUTING.md) 55 | 56 | ## License ⚖️ 57 | 58 | Apache License, Version 2.0 59 | 60 | {{ template "chart.sourcesSection" . }} 61 | 62 | {{ template "chart.maintainersSection" . }} 63 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "pulumi-kubernetes-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "pulumi-kubernetes-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "pulumi-kubernetes-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "pulumi-kubernetes-operator.labels" -}} 37 | helm.sh/chart: {{ include "pulumi-kubernetes-operator.chart" . }} 38 | {{ include "pulumi-kubernetes-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "pulumi-kubernetes-operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "pulumi-kubernetes-operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "pulumi-kubernetes-operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "pulumi-kubernetes-operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Create the advertised address for the controller 66 | */}} 67 | {{- define "pulumi-kubernetes-operator.advertisedAddress" -}} 68 | {{- if .Values.controller.advertisedAddress }} 69 | {{- .Values.controller.advertisedAddress }} 70 | {{- else }} 71 | {{- include "pulumi-kubernetes-operator.fullname" . }}.$(POD_NAMESPACE) 72 | {{- end }} 73 | {{- end }} 74 | 75 | 76 | {{/* 77 | Create the name of the image to use 78 | */}} 79 | {{- define "pulumi-kubernetes-operator.imageName" -}} 80 | {{- .Values.image.registry }}/{{- .Values.image.repository }}:{{- .Values.image.tag | default .Chart.AppVersion }} 81 | {{- end }} 82 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.createClusterRole }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.fullname" . }} 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-controller-manager 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/edit_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.createClusterAggregationRoles }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-edit 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 9 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 10 | rules: 11 | - apiGroups: 12 | - pulumi.com 13 | - auto.pulumi.com 14 | resources: 15 | - '*' 16 | verbs: 17 | - create 18 | - delete 19 | - deletecollection 20 | - patch 21 | - update 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-leader-election-role 5 | labels: 6 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - configmaps 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - create 17 | - update 18 | - patch 19 | - delete 20 | - apiGroups: 21 | - coordination.k8s.io 22 | resources: 23 | - leases 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | - create 29 | - update 30 | - patch 31 | - delete 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - events 36 | verbs: 37 | - create 38 | - patch -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-leader-election-rolebinding 5 | labels: 6 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: Role 10 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-leader-election-role 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 14 | namespace: {{ .Release.Namespace | quote }} -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.createRole }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-controller-manager 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | rules: 9 | {{- if .Values.rbac.extraRules }} 10 | {{- toYaml .Values.rbac.extraRules | nindent 2 }} 11 | {{- end }} 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - events 16 | verbs: 17 | - create 18 | - patch 19 | - update 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - secrets 24 | verbs: 25 | - create 26 | - get 27 | - list 28 | - watch 29 | - apiGroups: 30 | - apps 31 | resources: 32 | - statefulsets 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - auto.pulumi.com 43 | resources: 44 | - updates 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - auto.pulumi.com 55 | resources: 56 | - updates/finalizers 57 | verbs: 58 | - update 59 | - apiGroups: 60 | - auto.pulumi.com 61 | resources: 62 | - updates/status 63 | verbs: 64 | - get 65 | - patch 66 | - update 67 | - apiGroups: 68 | - auto.pulumi.com 69 | resources: 70 | - workspaces 71 | verbs: 72 | - create 73 | - delete 74 | - get 75 | - list 76 | - patch 77 | - update 78 | - watch 79 | - apiGroups: 80 | - auto.pulumi.com 81 | resources: 82 | - workspaces/finalizers 83 | verbs: 84 | - update 85 | - apiGroups: 86 | - auto.pulumi.com 87 | resources: 88 | - workspaces/rpc 89 | verbs: 90 | - use 91 | - apiGroups: 92 | - auto.pulumi.com 93 | resources: 94 | - workspaces/status 95 | verbs: 96 | - get 97 | - patch 98 | - update 99 | - apiGroups: 100 | - "" 101 | resources: 102 | - pods 103 | verbs: 104 | - create 105 | - delete 106 | - get 107 | - list 108 | - patch 109 | - update 110 | - watch 111 | - apiGroups: 112 | - "" 113 | resources: 114 | - services 115 | verbs: 116 | - create 117 | - delete 118 | - get 119 | - list 120 | - patch 121 | - update 122 | - watch 123 | - apiGroups: 124 | - pulumi.com 125 | resources: 126 | - programs 127 | verbs: 128 | - create 129 | - delete 130 | - get 131 | - list 132 | - patch 133 | - update 134 | - watch 135 | - apiGroups: 136 | - pulumi.com 137 | resources: 138 | - programs/finalizers 139 | verbs: 140 | - update 141 | - apiGroups: 142 | - pulumi.com 143 | resources: 144 | - programs/status 145 | verbs: 146 | - get 147 | - patch 148 | - update 149 | - apiGroups: 150 | - pulumi.com 151 | resources: 152 | - stacks 153 | verbs: 154 | - create 155 | - delete 156 | - get 157 | - list 158 | - patch 159 | - update 160 | - watch 161 | - apiGroups: 162 | - pulumi.com 163 | resources: 164 | - stacks/finalizers 165 | verbs: 166 | - update 167 | - apiGroups: 168 | - pulumi.com 169 | resources: 170 | - stacks/status 171 | verbs: 172 | - get 173 | - patch 174 | - update 175 | - apiGroups: 176 | - source.toolkit.fluxcd.io 177 | resources: 178 | - buckets 179 | verbs: 180 | - get 181 | - list 182 | - watch 183 | - apiGroups: 184 | - source.toolkit.fluxcd.io 185 | resources: 186 | - gitrepositories 187 | verbs: 188 | - get 189 | - list 190 | - watch 191 | - apiGroups: 192 | - source.toolkit.fluxcd.io 193 | resources: 194 | - ocirepositories 195 | verbs: 196 | - get 197 | - list 198 | - watch 199 | {{- end }} 200 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.createRole }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.fullname" . }} 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-controller-manager 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | {{ end }} -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "pulumi-kubernetes-operator.fullname" . }} 5 | labels: 6 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 7 | {{- with .Values.serviceAnnotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - name: http-fileserver 15 | port: 80 16 | protocol: TCP 17 | targetPort: http-fileserver 18 | - name: http-metrics 19 | port: 8383 20 | targetPort: http-metrics 21 | protocol: TCP 22 | selector: 23 | {{- include "pulumi-kubernetes-operator.selectorLabels" . | nindent 4 }} 24 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/service_account.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.serviceMonitor.enabled }} 2 | {{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }} 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | {{- if .Values.serviceMonitor.labels }} 7 | labels: 8 | {{ toYaml .Values.serviceMonitor.labels | indent 4}} 9 | {{- end }} 10 | name: {{ template "pulumi-kubernetes-operator.fullname" . }} 11 | {{- if .Values.serviceMonitor.namespace }} 12 | namespace: {{ .Values.serviceMonitor.namespace }} 13 | {{- end }} 14 | spec: 15 | endpoints: 16 | - targetPort: 8383 17 | {{- if .Values.serviceMonitor.interval }} 18 | interval: {{ .Values.serviceMonitor.interval }} 19 | {{- end }} 20 | {{- if .Values.serviceMonitor.telemetryPath }} 21 | path: {{ .Values.serviceMonitor.telemetryPath }} 22 | {{- end }} 23 | {{- if .Values.serviceMonitor.timeout }} 24 | scrapeTimeout: {{ .Values.serviceMonitor.timeout }} 25 | {{- end }} 26 | {{- if .Values.serviceMonitor.metricRelabelings }} 27 | metricRelabelings: 28 | {{ toYaml .Values.serviceMonitor.metricRelabelings | indent 4 }} 29 | {{- end }} 30 | jobLabel: {{ template "pulumi-kubernetes-operator.fullname" . }} 31 | namespaceSelector: 32 | matchNames: 33 | - {{ .Release.Namespace }} 34 | selector: 35 | matchLabels: 36 | {{- include "pulumi-kubernetes-operator.selectorLabels" . | nindent 6 }} 37 | {{- if .Values.serviceMonitor.targetLabels }} 38 | targetLabels: 39 | {{- range .Values.serviceMonitor.targetLabels }} 40 | - {{ . }} 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/token_request_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-token-request-role 5 | labels: 6 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - serviceaccounts/token 12 | resourceNames: 13 | - {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 14 | verbs: 15 | - create 16 | -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/token_request_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-token-request-rolebinding 5 | labels: 6 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: Role 10 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-token-request-role 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ include "pulumi-kubernetes-operator.serviceAccountName" . }} 14 | namespace: {{ .Release.Namespace | quote }} -------------------------------------------------------------------------------- /deploy/helm/pulumi-operator/templates/view_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.createClusterAggregationRoles }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "pulumi-kubernetes-operator.fullname" . }}-view 6 | labels: 7 | {{- include "pulumi-kubernetes-operator.labels" . | nindent 4 }} 8 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 9 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 10 | rbac.authorization.k8s.io/aggregate-to-view: "true" 11 | rules: 12 | - apiGroups: 13 | - pulumi.com 14 | - auto.pulumi.com 15 | resources: 16 | - '*' 17 | verbs: 18 | - get 19 | - list 20 | - watch 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /deploy/operator_template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: pulumi-kubernetes-operator 6 | spec: 7 | # Currently only 1 replica supported, until leader election: https://github.com/pulumi/pulumi-kubernetes-operator/issues/33 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | name: pulumi-kubernetes-operator 12 | template: 13 | metadata: 14 | labels: 15 | name: pulumi-kubernetes-operator 16 | spec: 17 | serviceAccountName: pulumi-kubernetes-operator 18 | volumes: 19 | - name: tmp-dir 20 | emptyDir: {} 21 | containers: 22 | - name: pulumi-kubernetes-operator 23 | image: : 24 | args: 25 | - "--zap-level=error" 26 | - "--zap-time-encoding=iso8601" 27 | volumeMounts: 28 | - mountPath: /tmp 29 | name: tmp-dir 30 | env: 31 | - name: WATCH_NAMESPACE 32 | valueFrom: 33 | fieldRef: 34 | fieldPath: metadata.namespace 35 | - name: POD_NAME 36 | valueFrom: 37 | fieldRef: 38 | fieldPath: metadata.name 39 | - name: OPERATOR_NAME 40 | value: "pulumi-kubernetes-operator" 41 | - name: GRACEFUL_SHUTDOWN_TIMEOUT_DURATION 42 | value: "5m" 43 | - name: MAX_CONCURRENT_RECONCILES 44 | value: "10" 45 | - name: PULUMI_INFER_NAMESPACE 46 | value: "1" 47 | terminationGracePeriodSeconds: 300 # Should be same or larger than GRACEFUL_SHUTDOWN_TIMEOUT_DURATION 48 | -------------------------------------------------------------------------------- /docs/images/sample-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulumi/pulumi-kubernetes-operator/8cd230c09124f6077270abf12855d58b95152e29/docs/images/sample-dashboard.png -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## General Approach 4 | 5 | For an overview of the general architecture of the Pulumi Kubernetes Operator, see 6 | ["What’s New in Pulumi Kubernetes Operator 2.0?"](https://www.pulumi.com/blog/pulumi-kubernetes-operator-2-0/#whats-new-in-pulumi-kubernetes-operator-20). 7 | 8 | Here's some tips for troubleshooting issues with stack deployments. 9 | 10 | ### Watch the Kubernetes events 11 | 12 | Use `kubectl` to watch for Kubernetes events in a given namespace: 13 | 14 | ```bash 15 | kubectl get events -n default --watch 16 | ``` 17 | 18 | Key events to watch for: 19 | 20 | - `StackUpdateDetected` - the controller intends to perform a Pulumi stack update (`pulumi up`). 21 | - `ConnectionFailure` - the operator is unable to connect to the workspace pod. 22 | - `InstallationFailure` - the `pulumi install` command is failing within the workspace pod. 23 | - `Initialized` - a workspace pod was successfully initialized with your program source code and is ready to perform stack updates. 24 | - `UpdateSucceeded` - a stack update succeeeded. 25 | - `UpdateFailed` - a stack update failed. 26 | - `StackCreated` - the stack is now up-to-date 27 | 28 | ### Check the Status information 29 | 30 | The Stack object has a rich status field with information about the latest update activity and about conditions 31 | affecting the stack deployment. Use `kubectl describe stack $STACK_NAME` to see the status block. 32 | 33 | Look for: 34 | - `.status.lastUpdate` - key information about the last stack update that was applied. 35 | - `.status.currentUpdate` - information about a planned or ongoing stack update. 36 | - `.status.conditions` - conditions affecting the stack, including important messages. 37 | 38 | ### Monitor the workspace pod logs 39 | 40 | The workspace pod is where Pulumi deployment activity happens. Use `kubectl logs` (or [stern](https://github.com/stern/stern)) 41 | to see log output from the Pulumi CLI. 42 | 43 | ### Force a Update 44 | 45 | When a stack fails to deploy for any reason, the system applies a back-off retry strategy. After a few tries, 46 | the system waits for an hour or longer before trying again. 47 | 48 | It is possible to force the operator to make another attempt, simply by annotating your stack object as shown below. 49 | 50 | ```bash 51 | kubectl annotate stack $STACK_NAME "pulumi.com/reconciliation-request=$(date)" --overwrite 52 | ``` 53 | 54 | ### Get a Shell to a Workspace Pod 55 | 56 | By default, the operator leaves the workspace pod in-place after running a stack update. Feel free to use the workspace 57 | pod for interactive troubleshooting and to use the Pulumi CLI. For example, to get a shell to the workspace pod for 58 | a Stack named `random-yaml`, use the following command: 59 | 60 | ```bash 61 | kubectl exec -i -t random-yaml-git-workspace-0 --container pulumi -- bash 62 | ``` 63 | 64 | The default directory is the location of your program source code. You should be able to run Pulumi CLI commands from there. 65 | 66 | ### Enable Pulumi Verbose Logging 67 | 68 | The Stack API provides a convenient option for increasing Pulumi's log verbosity: 69 | 70 | ```diff 71 | apiVersion: pulumi.com/v1 72 | kind: Stack 73 | spec: 74 | + pulumiLogLevel: 10 75 | ``` 76 | 77 | ## Specific Issues 78 | 79 | ### Fetching from Flux Source 80 | 81 | The `fetch` init container is responsible for fetching your program source code. When using a Flux source, 82 | the pod connects to the `source-controller` pod in the `flux-system` namespace. 83 | 84 | If your cluster has [Network Policy](https://kubernetes.io/docs/concepts/services-networking/network-policies/) enabled, 85 | a policy must be configured to allow the above traffic. Apply this manifest: [operator/config/flux/network_policy.yaml](./operator/config/flux/network_policy.yaml). 86 | 87 | ### Stack Conflicts 88 | 89 | On rare occasion, your Pulumi stack may become locked in the state backend. To unlock your stack: 90 | 1. Get a shell to your workspace pod (see above). 91 | 2. Run `pulumi cancel`. 92 | -------------------------------------------------------------------------------- /examples/custom-source/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pulumi/pulumi:3.175.0-nonroot@sha256:a2ca650674c2a08b586e56a8e514c73e28f0761c5c0c4a1e3acdee49feec082a 2 | 3 | # Clone the git repository 4 | RUN git clone --depth 1 https://github.com/pulumi/examples.git /home/pulumi/examples 5 | 6 | # Preinstall the plugins for the 'stack-readme-py' program 7 | RUN pulumi install -C /home/pulumi/examples/stack-readme-py -------------------------------------------------------------------------------- /examples/custom-source/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | docker build --platform=linux/amd64 -t localhost:5000/pulumi-and-app:latest . -------------------------------------------------------------------------------- /examples/custom-source/README.md: -------------------------------------------------------------------------------- 1 | Demonstrates how to use your own program source rather than relying on built-in support for Git and Flux. 2 | 3 | To use your own source, you have two options: 4 | 5 | 1. Use an init container to copy Pulumi project files to `/share/workspace`, or 6 | 2. Bake your Pulumi project files into the workspace image. 7 | 8 | This example explores the second option. The procedure is: 9 | 10 | 1. Build a Docker image containing your project files. See the example Dockerfile that clones Pulumi's repository of examples. It also runs `pulumi install` to pre-install your program's dependencies. 11 | 2. In the `Stack` specification, customize the workspace template to point to a Pulumi project within the cloned repository, using `.spec.local.dir`. 12 | 3. Deploy the stack. The system automatically links to the local directory. -------------------------------------------------------------------------------- /examples/custom-source/stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: custom-source 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: custom-source:system:auth-delegator 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: system:auth-delegator 15 | subjects: 16 | - kind: ServiceAccount 17 | name: custom-source 18 | namespace: default 19 | --- 20 | apiVersion: pulumi.com/v1 21 | kind: Stack 22 | metadata: 23 | name: custom-source 24 | namespace: default 25 | spec: 26 | serviceAccountName: custom-source 27 | stack: dev 28 | refresh: true 29 | continueResyncOnCommitMatch: true 30 | resyncFrequencySeconds: 60 31 | destroyOnFinalize: true 32 | envRefs: 33 | PULUMI_ACCESS_TOKEN: 34 | type: Secret 35 | secret: 36 | name: pulumi-api-secret 37 | key: accessToken 38 | workspaceTemplate: 39 | spec: 40 | image: localhost:5000/pulumi-and-app:latest 41 | imagePullPolicy: IfNotPresent 42 | local: 43 | dir: /home/pulumi/examples/stack-readme-py 44 | -------------------------------------------------------------------------------- /examples/custom-workspace/workspace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: test-secret 5 | data: 6 | username: bXktYXBw 7 | password: Mzk1MjgkdmRnN0pi 8 | --- 9 | apiVersion: auto.pulumi.com/v1alpha1 10 | kind: Workspace 11 | metadata: 12 | name: random-yaml-1e2fc47 13 | spec: 14 | image: pulumi/pulumi:3.147.0-nonroot 15 | securityProfile: restricted 16 | serviceAccountName: default 17 | git: 18 | url: https://github.com/pulumi/examples.git 19 | revision: 1e2fc471709448f3c9f7a250f28f1eafcde7017b 20 | dir: random-yaml 21 | env: 22 | - name: PULUMI_ACCESS_TOKEN 23 | valueFrom: 24 | secretKeyRef: 25 | name: pulumi-api-secret 26 | key: accessToken 27 | resources: 28 | requests: 29 | cpu: 1 30 | memory: 512Mi 31 | limits: 32 | cpu: 1 33 | memory: 512Mi 34 | 35 | # various extension points shown here. 36 | # - custom pod labels 37 | # - pod tolerations 38 | # - extra init container(s) 39 | # - extra volume(s) and volume mounts onto the 'pulumi' container 40 | podTemplate: 41 | metadata: 42 | labels: 43 | example.com/mylabel: bar 44 | spec: 45 | terminationGracePeriodSeconds: 3600 46 | tolerations: 47 | - key: "example.com/foo" 48 | operator: "Exists" 49 | effect: "NoSchedule" 50 | initContainers: 51 | - name: extra 52 | image: busybox 53 | command: ["sh", "-c", "echo 'Hello, extra init container!'"] 54 | securityContext: 55 | allowPrivilegeEscalation: false 56 | capabilities: 57 | add: 58 | - NET_BIND_SERVICE 59 | drop: 60 | - ALL 61 | volumeMounts: 62 | - name: share 63 | mountPath: /share 64 | containers: 65 | - name: pulumi 66 | volumeMounts: 67 | - name: secret-volume 68 | mountPath: /etc/secret-volume 69 | readOnly: true 70 | - name: oidc-token 71 | mountPath: /var/run/secrets/pulumi 72 | volumes: 73 | - name: secret-volume 74 | secret: 75 | secretName: test-secret 76 | - name: oidc-token 77 | projected: 78 | sources: 79 | - serviceAccountToken: 80 | audience: urn:pulumi:org:ORG_NAME 81 | path: token 82 | expirationSeconds: 3600 83 | stacks: 84 | - name: dev 85 | config: 86 | - key: "pulumi:oidcToken" 87 | valueFrom: 88 | path: /var/run/secrets/pulumi/token 89 | secret: true 90 | - key: kubernetes:namespace 91 | value: "default" 92 | - key: data.active 93 | path: true 94 | value: "true" 95 | - key: data.nums[0] 96 | path: true 97 | value: "1" 98 | - key: data.nums[1] 99 | path: true 100 | value: "2" 101 | -------------------------------------------------------------------------------- /examples/flux-source/repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: GitRepository 3 | metadata: 4 | name: pulumi-examples 5 | spec: 6 | interval: 24h 7 | ref: 8 | branch: master 9 | timeout: 60s 10 | url: https://github.com/pulumi/examples -------------------------------------------------------------------------------- /examples/flux-source/stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: random-yaml-flux 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: random-yaml-flux:system:auth-delegator 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: system:auth-delegator 15 | subjects: 16 | - kind: ServiceAccount 17 | name: random-yaml-flux 18 | namespace: default 19 | --- 20 | apiVersion: pulumi.com/v1 21 | kind: Stack 22 | metadata: 23 | name: random-yaml-flux 24 | namespace: default 25 | spec: 26 | serviceAccountName: random-yaml-flux 27 | fluxSource: 28 | sourceRef: 29 | apiVersion: source.toolkit.fluxcd.io/v1 30 | kind: GitRepository 31 | name: pulumi-examples 32 | dir: random-yaml/ 33 | stack: random-yaml-flux 34 | refresh: true 35 | destroyOnFinalize: true 36 | envRefs: 37 | PULUMI_ACCESS_TOKEN: 38 | type: Secret 39 | secret: 40 | name: pulumi-api-secret 41 | key: accessToken 42 | workspaceTemplate: 43 | spec: 44 | image: pulumi/pulumi:3.147.0-nonroot 45 | 46 | -------------------------------------------------------------------------------- /examples/git-source/stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: random-yaml-git 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: random-yaml-git:system:auth-delegator 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: system:auth-delegator 15 | subjects: 16 | - kind: ServiceAccount 17 | name: random-yaml-git 18 | namespace: default 19 | --- 20 | apiVersion: pulumi.com/v1 21 | kind: Stack 22 | metadata: 23 | name: random-yaml-git 24 | namespace: default 25 | spec: 26 | serviceAccountName: random-yaml-git 27 | projectRepo: https://github.com/pulumi/examples 28 | branch: master 29 | shallow: true 30 | repoDir: random-yaml/ 31 | stack: random-yaml-git 32 | refresh: true 33 | destroyOnFinalize: true 34 | envRefs: 35 | PULUMI_ACCESS_TOKEN: 36 | type: Secret 37 | secret: 38 | name: pulumi-api-secret 39 | key: accessToken 40 | workspaceTemplate: 41 | spec: 42 | image: pulumi/pulumi:3.147.0-nonroot 43 | 44 | -------------------------------------------------------------------------------- /examples/program-source/program.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pulumi.com/v1 2 | kind: Program 3 | metadata: 4 | name: random-yaml 5 | program: 6 | resources: 7 | randomPassword: 8 | type: random:RandomPassword 9 | properties: 10 | length: 16 11 | special: true 12 | overrideSpecial: "_%@" 13 | 14 | outputs: 15 | password: ${randomPassword.result} -------------------------------------------------------------------------------- /examples/program-source/stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: random-yaml-program 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: random-yaml-program:system:auth-delegator 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: system:auth-delegator 15 | subjects: 16 | - kind: ServiceAccount 17 | name: random-yaml-program 18 | namespace: default 19 | --- 20 | apiVersion: pulumi.com/v1 21 | kind: Stack 22 | metadata: 23 | name: random-yaml-program 24 | namespace: default 25 | spec: 26 | serviceAccountName: random-yaml-program 27 | programRef: 28 | name: random-yaml 29 | stack: random-yaml-program 30 | refresh: true 31 | destroyOnFinalize: true 32 | envRefs: 33 | PULUMI_ACCESS_TOKEN: 34 | type: Secret 35 | secret: 36 | name: pulumi-api-secret 37 | key: accessToken 38 | workspaceTemplate: 39 | spec: 40 | image: pulumi/pulumi:3.147.0-nonroot 41 | 42 | -------------------------------------------------------------------------------- /examples/pulumi-ts/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /examples/pulumi-ts/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: pulumi-ts 2 | description: A Pulumi program to deploy a Stack using the Pulumi Kubernetes Operator 3 | runtime: 4 | name: nodejs 5 | options: 6 | packagemanager: npm 7 | config: 8 | pulumi:tags: 9 | value: 10 | pulumi:template: kubernetes-typescript 11 | -------------------------------------------------------------------------------- /examples/pulumi-ts/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as k8s from "@pulumi/kubernetes"; 3 | import * as pulumiservice from "@pulumi/pulumiservice"; 4 | 5 | // Create a Kubernetes ServiceAccount for the Pulumi workspace pod 6 | const sa = new k8s.core.v1.ServiceAccount("random-yaml", {}); 7 | 8 | // Grant system:auth-delegator to the ServiceAccount 9 | const crb = new k8s.rbac.v1.ClusterRoleBinding("random-yaml:system:auth-delegator", { 10 | roleRef: { 11 | apiGroup: "rbac.authorization.k8s.io", 12 | kind: "ClusterRole", 13 | name: "system:auth-delegator", 14 | }, 15 | subjects: [{ 16 | kind: "ServiceAccount", 17 | name: sa.metadata.name, 18 | namespace: sa.metadata.namespace, 19 | }], 20 | }); 21 | 22 | // Provision a Pulumi Cloud access token and store it in a Kubernetes Secret 23 | const accessToken = new pulumiservice.AccessToken("random-yaml", { 24 | description: `For stack "${pulumi.runtime.getOrganization()}/${pulumi.runtime.getProject()}/${pulumi.runtime.getStack()}"`, 25 | }); 26 | const apiSecret = new k8s.core.v1.Secret("random-yaml", { 27 | stringData: { 28 | "accessToken": accessToken.value, 29 | } 30 | }); 31 | 32 | // Deploy the "random-yaml" program from the github.com/pulumi/examples repository. 33 | const stack = new k8s.apiextensions.CustomResource("random-yaml", { 34 | apiVersion: 'pulumi.com/v1', 35 | kind: 'Stack', 36 | spec: { 37 | serviceAccountName: sa.metadata.name, 38 | projectRepo: "https://github.com/pulumi/examples", 39 | repoDir: "random-yaml/", 40 | branch: "master", 41 | shallow: true, 42 | stack: "pulumi-ts", 43 | refresh: true, 44 | destroyOnFinalize: true, 45 | envRefs: { 46 | PULUMI_ACCESS_TOKEN: { 47 | type: "Secret", 48 | secret: { 49 | name: apiSecret.metadata.name, 50 | key: "accessToken", 51 | }, 52 | }, 53 | }, 54 | workspaceTemplate: { 55 | spec: { 56 | image: "pulumi/pulumi:3.147.0-nonroot", 57 | }, 58 | }, 59 | }, 60 | }, {dependsOn: [sa, crb, apiSecret]}); 61 | 62 | // export const stackName = stack.metadata.name; -------------------------------------------------------------------------------- /examples/pulumi-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulumi-ts", 3 | "main": "index.ts", 4 | "devDependencies": { 5 | "@types/node": "^18", 6 | "typescript": "^5.0.0" 7 | }, 8 | "dependencies": { 9 | "@pulumi/kubernetes": "4.23.0", 10 | "@pulumi/pulumi": "3.175.0", 11 | "@pulumi/pulumiservice": "0.29.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/pulumi-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "bin", 5 | "target": "es2020", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "pretty": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "files": [ 16 | "index.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /operator/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/* 9 | Dockerfile.cross 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | __debug_bin* 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Go workspace file 19 | go.work 20 | 21 | # Kubernetes Generated files - skip generated files, except for vendored files 22 | !vendor/**/zz_generated.* 23 | 24 | # editor and IDE paraphernalia 25 | .idea 26 | .vscode 27 | *.swp 28 | *.swo 29 | *~ 30 | -------------------------------------------------------------------------------- /operator/PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: pulumi.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | multigroup: true 9 | plugins: 10 | manifests.sdk.operatorframework.io/v2: {} 11 | scorecard.sdk.operatorframework.io/v2: {} 12 | projectName: pulumi-kubernetes-operator 13 | repo: github.com/pulumi/pulumi-kubernetes-operator/operator 14 | resources: 15 | - api: 16 | crdVersion: v1 17 | namespaced: true 18 | controller: true 19 | domain: pulumi.com 20 | group: auto 21 | kind: Workspace 22 | path: github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1 23 | version: v1alpha1 24 | - api: 25 | crdVersion: v1 26 | namespaced: true 27 | controller: true 28 | domain: pulumi.com 29 | group: auto 30 | kind: Update 31 | path: github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1 32 | version: v1alpha1 33 | - api: 34 | crdVersion: v1 35 | namespaced: true 36 | controller: true 37 | group: pulumi.com 38 | kind: Stack 39 | path: github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/v1alpha1 40 | version: v1alpha1 41 | - api: 42 | crdVersion: v1 43 | namespaced: true 44 | controller: true 45 | group: pulumi.com 46 | kind: Program 47 | path: github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/v1 48 | version: v1 49 | version: "3" 50 | -------------------------------------------------------------------------------- /operator/README.md: -------------------------------------------------------------------------------- 1 | # pulumi-kubernetes-operator 2 | // TODO(user): Add simple overview of use/purpose 3 | 4 | ## Description 5 | // TODO(user): An in-depth paragraph about your project and overview of use 6 | 7 | ## Getting Started 8 | 9 | ### Prerequisites 10 | - go version v1.20.0+ 11 | - docker version 17.03+. 12 | - kubectl version v1.11.3+. 13 | - Access to a Kubernetes v1.11.3+ cluster. 14 | 15 | ### To Deploy on the cluster 16 | **Build and push your image to the location specified by `IMG`:** 17 | 18 | ```sh 19 | make docker-build docker-push IMG=/pulumi-kubernetes-operator:tag 20 | ``` 21 | 22 | **NOTE:** This image ought to be published in the personal registry you specified. 23 | And it is required to have access to pull the image from the working environment. 24 | Make sure you have the proper permission to the registry if the above commands don’t work. 25 | 26 | **Install the CRDs into the cluster:** 27 | 28 | ```sh 29 | make install 30 | ``` 31 | 32 | **Deploy the Manager to the cluster with the image specified by `IMG`:** 33 | 34 | ```sh 35 | make deploy IMG=/pulumi-kubernetes-operator:tag 36 | ``` 37 | 38 | > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin 39 | privileges or be logged in as admin. 40 | 41 | **Create instances of your solution** 42 | You can apply the samples (examples) from the config/sample: 43 | 44 | ```sh 45 | kubectl apply -k config/samples/ 46 | ``` 47 | 48 | >**NOTE**: Ensure that the samples has default values to test it out. 49 | 50 | ### To Uninstall 51 | **Delete the instances (CRs) from the cluster:** 52 | 53 | ```sh 54 | kubectl delete -k config/samples/ 55 | ``` 56 | 57 | **Delete the APIs(CRDs) from the cluster:** 58 | 59 | ```sh 60 | make uninstall 61 | ``` 62 | 63 | **UnDeploy the controller from the cluster:** 64 | 65 | ```sh 66 | make undeploy 67 | ``` 68 | 69 | ## Contributing 70 | // TODO(user): Add detailed information on how you would like others to contribute to this project 71 | 72 | **NOTE:** Run `make help` for more information on all potential `make` targets 73 | 74 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 75 | 76 | ## License 77 | 78 | Copyright 2024. 79 | 80 | Licensed under the Apache License, Version 2.0 (the "License"); 81 | you may not use this file except in compliance with the License. 82 | You may obtain a copy of the License at 83 | 84 | http://www.apache.org/licenses/LICENSE-2.0 85 | 86 | Unless required by applicable law or agreed to in writing, software 87 | distributed under the License is distributed on an "AS IS" BASIS, 88 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 89 | See the License for the specific language governing permissions and 90 | limitations under the License. 91 | 92 | -------------------------------------------------------------------------------- /operator/api/auto/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1alpha1 contains APIs for Pulumi Automation APIs. 16 | // 17 | // TODO(https://github.com/kubernetes/code-generator/issues/150) 18 | // +groupName=auto.pulumi.com 19 | package v1alpha1 20 | -------------------------------------------------------------------------------- /operator/api/auto/v1alpha1/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | type event struct { 18 | reason eventReason 19 | eventType eventType 20 | } 21 | 22 | func (e event) EventType() string { 23 | return string(e.eventType) 24 | } 25 | 26 | func (e event) Reason() string { 27 | return string(e.reason) 28 | } 29 | 30 | // eventType tracks the types supported by the Kubernetes EventRecorder interface in k8s.io/client-go/tools/record 31 | type eventType string 32 | 33 | const ( 34 | EventTypeNormal eventType = "Normal" 35 | EventTypeWarning eventType = "Warning" 36 | ) 37 | 38 | // eventReason reflects distinct categorizations of events emitted by the stack controller. 39 | type eventReason string 40 | 41 | const ( 42 | // Warnings 43 | Migrated eventReason = "Migrated" 44 | ConnectionFailure eventReason = "ConnectionFailure" 45 | InstallationFailure eventReason = "InstallationFailure" 46 | StackInitializationFailure eventReason = "StackInitializationFailure" 47 | UpdateFailed eventReason = "UpdateFailed" 48 | 49 | // Normals 50 | Initialized eventReason = "Initialized" 51 | UpdateExpired eventReason = "UpdateExpired" 52 | UpdateSucceeded eventReason = "UpdateSucceeded" 53 | ) 54 | 55 | func MigratedEvent() event { 56 | return event{eventType: EventTypeWarning, reason: Migrated} 57 | } 58 | 59 | func ConnectionFailureEvent() event { 60 | return event{eventType: EventTypeWarning, reason: ConnectionFailure} 61 | } 62 | 63 | func InstallationFailureEvent() event { 64 | return event{eventType: EventTypeWarning, reason: InstallationFailure} 65 | } 66 | 67 | func StackInitializationFailureEvent() event { 68 | return event{eventType: EventTypeWarning, reason: StackInitializationFailure} 69 | } 70 | 71 | func UpdateFailedEvent() event { 72 | return event{eventType: EventTypeWarning, reason: UpdateFailed} 73 | } 74 | 75 | func InitializedEvent() event { 76 | return event{eventType: EventTypeNormal, reason: Initialized} 77 | } 78 | 79 | func UpdateExpiredEvent() event { 80 | return event{eventType: EventTypeNormal, reason: UpdateExpired} 81 | } 82 | 83 | func UpdateSucceededEvent() event { 84 | return event{eventType: EventTypeNormal, reason: UpdateSucceeded} 85 | } 86 | -------------------------------------------------------------------------------- /operator/api/auto/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1alpha1 contains API Schema definitions for the auto v1alpha1 API group 16 | // +kubebuilder:object:generate=true 17 | // +groupName=auto.pulumi.com 18 | package v1alpha1 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | var ( 26 | // SchemeGroupVersion is group version used to register these objects 27 | SchemeGroupVersion = schema.GroupVersion{Group: "auto.pulumi.com", Version: "v1alpha1"} 28 | 29 | // GroupVersion is for kubebuilder<->applyconfiguration-gen compatibility. 30 | // TODO(https://github.com/kubernetes-sigs/kubebuilder/issues/3692) 31 | GroupVersion = SchemeGroupVersion 32 | 33 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 34 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 35 | 36 | // AddToScheme adds the types in this group-version to the given scheme. 37 | AddToScheme = SchemeBuilder.AddToScheme 38 | ) 39 | -------------------------------------------------------------------------------- /operator/api/pulumi/shared/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package shared contains shared schema definitions for different API versions. 16 | // +k8s:deepcopy-gen=package 17 | // +groupName=pulumi.com 18 | package shared 19 | -------------------------------------------------------------------------------- /operator/api/pulumi/v1/artifact_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2024, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1 16 | 17 | import ( 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | ) 20 | 21 | // Artifact represents the output of a Source reconciliation. 22 | type Artifact struct { 23 | // Path is the relative file path of the Artifact. It can be used to locate 24 | // the file in the root of the Artifact storage on the local file system of 25 | // the controller managing the Source. 26 | // +required 27 | Path string `json:"path"` 28 | 29 | // URL is the HTTP address of the Artifact as exposed by the controller 30 | // managing the Source. It can be used to retrieve the Artifact for 31 | // consumption, e.g. by another controller applying the Artifact contents. 32 | // +required 33 | URL string `json:"url"` 34 | 35 | // Revision is a human-readable identifier traceable in the origin source 36 | // system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. 37 | // +required 38 | Revision string `json:"revision"` 39 | 40 | // Digest is the digest of the file in the form of ':'. 41 | // +optional 42 | // +kubebuilder:validation:Pattern="^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$" 43 | Digest string `json:"digest,omitempty"` 44 | 45 | // LastUpdateTime is the timestamp corresponding to the last update of the 46 | // Artifact. 47 | // +required 48 | LastUpdateTime metav1.Time `json:"lastUpdateTime"` 49 | 50 | // Size is the number of bytes in the file. 51 | // +optional 52 | Size *int64 `json:"size,omitempty"` 53 | 54 | // Metadata holds upstream information such as OCI annotations. 55 | // +optional 56 | Metadata map[string]string `json:"metadata,omitempty"` 57 | } 58 | 59 | // HasRevision returns if the given revision matches the current Revision of 60 | // the Artifact. 61 | func (in *Artifact) HasRevision(revision string) bool { 62 | if in == nil { 63 | return false 64 | } 65 | return in.Revision == revision 66 | } 67 | 68 | // HasDigest returns if the given digest matches the current Digest of the 69 | // Artifact. 70 | func (in *Artifact) HasDigest(digest string) bool { 71 | if in == nil { 72 | return false 73 | } 74 | return in.Digest == digest 75 | } 76 | -------------------------------------------------------------------------------- /operator/api/pulumi/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1 contains API Schema definitions for the pulumi.com v1 API group 16 | // +kubebuilder:object:generate=true 17 | // +groupName=pulumi.com 18 | package v1 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | var ( 26 | // GroupVersion is group version used to register these objects 27 | GroupVersion = schema.GroupVersion{Group: "pulumi.com", Version: "v1"} 28 | 29 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 30 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 31 | 32 | // AddToScheme adds the types in this group-version to the given scheme. 33 | AddToScheme = SchemeBuilder.AddToScheme 34 | ) 35 | -------------------------------------------------------------------------------- /operator/api/pulumi/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1alpha1 contains API Schema definitions for the pulumi.com v1alpha1 API group 16 | // +kubebuilder:object:generate=true 17 | // +groupName=pulumi.com 18 | package v1alpha1 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | var ( 26 | // GroupVersion is group version used to register these objects 27 | GroupVersion = schema.GroupVersion{Group: "pulumi.com", Version: "v1alpha1"} 28 | 29 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 30 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 31 | 32 | // AddToScheme adds the types in this group-version to the given scheme. 33 | AddToScheme = SchemeBuilder.AddToScheme 34 | ) 35 | -------------------------------------------------------------------------------- /operator/api/pulumi/v1alpha1/stack_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/shared" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | ) 21 | 22 | //+kubebuilder:object:root=true 23 | //+kubebuilder:resource:categories=pulumi 24 | //+kubebuilder:subresource:status 25 | 26 | // Stack is the Schema for the stacks API. 27 | // Deprecated: Note Stacks from pulumi.com/v1alpha1 is deprecated in favor of pulumi.com/v1. 28 | // It is completely backward compatible. Users are strongly encouraged to switch to pulumi.com/v1. 29 | type Stack struct { 30 | metav1.TypeMeta `json:",inline"` 31 | metav1.ObjectMeta `json:"metadata,omitempty"` 32 | 33 | Spec shared.StackSpec `json:"spec,omitempty"` 34 | Status shared.StackStatus `json:"status,omitempty"` 35 | } 36 | 37 | //+kubebuilder:object:root=true 38 | 39 | // StackList contains a list of Stack 40 | // Deprecated: Note Stack:ost from pulumi.com/v1alpha1 is deprecated in favor of pulumi.com/v1. 41 | // It is completely backward compatible. Users are strongly encouraged to switch to pulumi.com/v1. 42 | type StackList struct { 43 | metav1.TypeMeta `json:",inline"` 44 | metav1.ListMeta `json:"metadata,omitempty"` 45 | Items []Stack `json:"items"` 46 | } 47 | 48 | func init() { 49 | SchemeBuilder.Register(&Stack{}, &StackList{}) 50 | } 51 | -------------------------------------------------------------------------------- /operator/api/pulumi/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Copyright 2016-2025, Pulumi Corporation. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // Code generated by controller-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | runtime "k8s.io/apimachinery/pkg/runtime" 23 | ) 24 | 25 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 26 | func (in *Stack) DeepCopyInto(out *Stack) { 27 | *out = *in 28 | out.TypeMeta = in.TypeMeta 29 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 30 | in.Spec.DeepCopyInto(&out.Spec) 31 | in.Status.DeepCopyInto(&out.Status) 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stack. 35 | func (in *Stack) DeepCopy() *Stack { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(Stack) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 45 | func (in *Stack) DeepCopyObject() runtime.Object { 46 | if c := in.DeepCopy(); c != nil { 47 | return c 48 | } 49 | return nil 50 | } 51 | 52 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 53 | func (in *StackList) DeepCopyInto(out *StackList) { 54 | *out = *in 55 | out.TypeMeta = in.TypeMeta 56 | in.ListMeta.DeepCopyInto(&out.ListMeta) 57 | if in.Items != nil { 58 | in, out := &in.Items, &out.Items 59 | *out = make([]Stack, len(*in)) 60 | for i := range *in { 61 | (*in)[i].DeepCopyInto(&(*out)[i]) 62 | } 63 | } 64 | } 65 | 66 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackList. 67 | func (in *StackList) DeepCopy() *StackList { 68 | if in == nil { 69 | return nil 70 | } 71 | out := new(StackList) 72 | in.DeepCopyInto(out) 73 | return out 74 | } 75 | 76 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 77 | func (in *StackList) DeepCopyObject() runtime.Object { 78 | if c := in.DeepCopy(); c != nil { 79 | return c 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /operator/cmd/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "testing" 19 | 20 | _ "k8s.io/client-go/plugin/pkg/client/auth" 21 | ) 22 | 23 | func TestDetermineAdvAddr(t *testing.T) { 24 | const fakehostname = "fakehostname" 25 | t.Setenv("HOSTNAME", fakehostname) 26 | 27 | tests := []struct { 28 | addr string 29 | want string 30 | }{ 31 | { 32 | addr: ":9090", 33 | want: "localhost:9090", 34 | }, 35 | { 36 | addr: "localhost:1111", 37 | want: "localhost:1111", 38 | }, 39 | { 40 | addr: "0.0.0.0:9090", 41 | want: fakehostname + ":9090", 42 | }, 43 | { 44 | addr: "fake.default:9090", 45 | want: "fake.default:9090", 46 | }, 47 | { 48 | addr: "fake.default.svc.cluster.local:9090", 49 | want: "fake.default.svc.cluster.local:9090", 50 | }, 51 | } 52 | for _, tc := range tests { 53 | t.Run(tc.addr, func(t *testing.T) { 54 | if got := determineAdvAddr(tc.addr); got != tc.want { 55 | t.Errorf("determineAdvAddr() = %v, want %v", got, tc.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestEnvOrDefault(t *testing.T) { 62 | // Set up some ENV vars for testing. 63 | t.Setenv("TEST_ENV", "test") 64 | t.Setenv("EMPTY_ENV", "") 65 | 66 | tests := []struct { 67 | name string 68 | envName string 69 | defaultValue string 70 | want string 71 | }{ 72 | { 73 | name: "env set, default ignored", 74 | envName: "TEST_ENV", 75 | defaultValue: "default", 76 | want: "test", 77 | }, 78 | { 79 | name: "env not set, default used", 80 | envName: "EMPTY_ENV", 81 | defaultValue: "default", 82 | want: "default", 83 | }, 84 | { 85 | name: "env not set, no default", 86 | envName: "EMPTY_ENV", 87 | defaultValue: "", 88 | want: "", 89 | }, 90 | } 91 | for _, tc := range tests { 92 | t.Run(tc.name, func(t *testing.T) { 93 | if got := envOrDefault(tc.envName, tc.defaultValue); got != tc.want { 94 | t.Errorf("envOrDefault() = %v, want %v", got, tc.want) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /operator/config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - ./bases/auto.pulumi.com_updates.yaml 3 | - ./bases/auto.pulumi.com_workspaces.yaml 4 | - ./bases/pulumi.com_programs.yaml 5 | - ./bases/pulumi.com_stacks.yaml 6 | 7 | patches: 8 | -------------------------------------------------------------------------------- /operator/config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: pulumi-kubernetes-operator 2 | resources: 3 | - ../crd 4 | - ../rbac 5 | - ../manager 6 | - metrics_service.yaml 7 | 8 | patches: 9 | - path: manager_metrics_patch.yaml 10 | target: 11 | kind: Deployment -------------------------------------------------------------------------------- /operator/config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/1 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /operator/config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | protocol: TCP 13 | targetPort: 8443 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /operator/config/flux/network_policy.yaml: -------------------------------------------------------------------------------- 1 | # A network policy to allow Pulumi workspaces in the `default` namespace to 2 | # fetch Flux artifacts from the source-controller in the `flux-system` namespace. 3 | apiVersion: networking.k8s.io/v1 4 | kind: NetworkPolicy 5 | metadata: 6 | name: allow-pulumi-fetch-flux-artifacts 7 | namespace: flux-system 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | app: source-controller 12 | ingress: 13 | - ports: 14 | - protocol: TCP 15 | port: http 16 | from: 17 | - namespaceSelector: 18 | matchLabels: 19 | kubernetes.io/metadata.name: default 20 | podSelector: 21 | matchLabels: 22 | auto.pulumi.com/component: workspace 23 | policyTypes: 24 | - Ingress -------------------------------------------------------------------------------- /operator/config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | images: 4 | - name: controller 5 | newName: pulumi/pulumi-kubernetes-operator 6 | newTag: v2.1.0 7 | resources: 8 | - manager.yaml 9 | -------------------------------------------------------------------------------- /operator/config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: pulumi-kubernetes-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: pulumi-kubernetes-operator 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: pulumi-kubernetes-operator 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: pulumi-kubernetes-operator 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | replicas: 1 24 | template: 25 | metadata: 26 | annotations: 27 | kubectl.kubernetes.io/default-container: manager 28 | labels: 29 | control-plane: controller-manager 30 | spec: 31 | securityContext: 32 | runAsNonRoot: true 33 | runAsUser: 65532 34 | runAsGroup: 65532 35 | containers: 36 | - name: manager 37 | env: 38 | - name: POD_NAMESPACE 39 | valueFrom: 40 | fieldRef: 41 | fieldPath: metadata.namespace 42 | - name: POD_SA_NAME 43 | valueFrom: 44 | fieldRef: 45 | fieldPath: spec.serviceAccountName 46 | args: 47 | - /manager 48 | - --leader-elect 49 | - --health-probe-bind-address=:8081 50 | - --program-fs-adv-addr=pulumi-kubernetes-operator.$(POD_NAMESPACE):80 51 | - --zap-log-level=info 52 | - --zap-encoder=console 53 | - --zap-time-encoding=iso8601 54 | ports: 55 | - containerPort: 8383 56 | name: http-metrics 57 | protocol: TCP 58 | - containerPort: 9090 59 | name: http-fileserver 60 | protocol: TCP 61 | image: controller:latest 62 | imagePullPolicy: IfNotPresent 63 | securityContext: 64 | allowPrivilegeEscalation: false 65 | capabilities: 66 | drop: 67 | - "ALL" 68 | livenessProbe: 69 | httpGet: 70 | path: /healthz 71 | port: 8081 72 | initialDelaySeconds: 15 73 | periodSeconds: 20 74 | readinessProbe: 75 | httpGet: 76 | path: /readyz 77 | port: 8081 78 | initialDelaySeconds: 5 79 | periodSeconds: 10 80 | resources: 81 | limits: 82 | cpu: 200m 83 | memory: 128Mi 84 | requests: 85 | cpu: 200m 86 | memory: 128Mi 87 | serviceAccountName: controller-manager 88 | terminationGracePeriodSeconds: 600 89 | --- 90 | # Service is required to expose the file server for workspace pods to fetch Program objects. 91 | apiVersion: v1 92 | kind: Service 93 | metadata: 94 | name: pulumi-kubernetes-operator 95 | labels: 96 | control-plane: controller-manager 97 | spec: 98 | type: ClusterIP 99 | selector: 100 | control-plane: controller-manager 101 | ports: 102 | - name: http-fileserver 103 | port: 80 104 | protocol: TCP 105 | targetPort: http-fileserver 106 | - name: http-metrics 107 | port: 8383 108 | targetPort: http-metrics 109 | protocol: TCP -------------------------------------------------------------------------------- /operator/config/quickstart/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - ../default 3 | - service_account.yaml 4 | - rbac.yaml 5 | - network_policy.yaml 6 | -------------------------------------------------------------------------------- /operator/config/quickstart/network_policy.yaml: -------------------------------------------------------------------------------- 1 | # A network policy to allow Pulumi workspaces in the `default` namespace to 2 | # accept RPC traffic from the controller-manager in the `pulumi-kubernetes-operator` namespace. 3 | apiVersion: networking.k8s.io/v1 4 | kind: NetworkPolicy 5 | metadata: 6 | name: allow-workspace-rpc 7 | namespace: default 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | auto.pulumi.com/component: workspace 12 | ingress: 13 | - ports: 14 | - protocol: TCP 15 | port: grpc 16 | from: 17 | - namespaceSelector: 18 | matchLabels: 19 | kubernetes.io/metadata.name: pulumi-kubernetes-operator 20 | podSelector: 21 | matchLabels: 22 | app.kubernetes.io/name: pulumi-kubernetes-operator 23 | policyTypes: 24 | - Ingress -------------------------------------------------------------------------------- /operator/config/quickstart/rbac.yaml: -------------------------------------------------------------------------------- 1 | # Grant `system:auth-delegator` to the `default/pulumi` service account, 2 | # to enable Kubernetes RBAC for the Pulumi workspace. 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: default:pulumi:system:auth-delegator 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: system:auth-delegator # permissions: TokenReview, SubjectAccessReview 11 | subjects: 12 | - kind: ServiceAccount 13 | namespace: default 14 | name: pulumi 15 | -------------------------------------------------------------------------------- /operator/config/quickstart/service_account.yaml: -------------------------------------------------------------------------------- 1 | # A service account named `default/pulumi` for the Pulumi workspace (execution environment). 2 | # If your Pulumi program uses the Kubernetes resource provider, this service account will be used to 3 | # authenticate with the Kubernetes cluster. 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | namespace: default 8 | name: pulumi 9 | -------------------------------------------------------------------------------- /operator/config/rbac/edit.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: pulumi-kubernetes-operator-edit 5 | labels: 6 | app.kubernetes.io/name: pulumi-kubernetes-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 9 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 10 | rules: 11 | - apiGroups: 12 | - pulumi.com 13 | - auto.pulumi.com 14 | resources: ["*"] 15 | verbs: 16 | - create 17 | - delete 18 | - deletecollection 19 | - patch 20 | - update -------------------------------------------------------------------------------- /operator/config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - service_account.yaml 3 | - role.yaml 4 | - role_binding.yaml 5 | - leader_election_role.yaml 6 | - leader_election_role_binding.yaml 7 | - edit.yaml 8 | - view.yaml 9 | - metrics_auth_role.yaml 10 | - metrics_auth_role_binding.yaml 11 | - metrics_reader_role.yaml 12 | - token_request_role.yaml 13 | - token_request_role_binding.yaml -------------------------------------------------------------------------------- /operator/config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: pulumi-kubernetes-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /operator/config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: pulumi-kubernetes-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: pulumi-kubernetes-operator 16 | -------------------------------------------------------------------------------- /operator/config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /operator/config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /operator/config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /operator/config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: pulumi-kubernetes-operator-controller-manager 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - update 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - secrets 19 | verbs: 20 | - create 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - apps 26 | resources: 27 | - statefulsets 28 | verbs: 29 | - create 30 | - delete 31 | - get 32 | - list 33 | - patch 34 | - update 35 | - watch 36 | - apiGroups: 37 | - auto.pulumi.com 38 | resources: 39 | - updates 40 | verbs: 41 | - create 42 | - delete 43 | - get 44 | - list 45 | - patch 46 | - update 47 | - watch 48 | - apiGroups: 49 | - auto.pulumi.com 50 | resources: 51 | - updates/finalizers 52 | verbs: 53 | - update 54 | - apiGroups: 55 | - auto.pulumi.com 56 | resources: 57 | - updates/status 58 | verbs: 59 | - get 60 | - patch 61 | - update 62 | - apiGroups: 63 | - auto.pulumi.com 64 | resources: 65 | - workspaces 66 | verbs: 67 | - create 68 | - delete 69 | - get 70 | - list 71 | - patch 72 | - update 73 | - watch 74 | - apiGroups: 75 | - auto.pulumi.com 76 | resources: 77 | - workspaces/finalizers 78 | verbs: 79 | - update 80 | - apiGroups: 81 | - auto.pulumi.com 82 | resources: 83 | - workspaces/rpc 84 | verbs: 85 | - use 86 | - apiGroups: 87 | - auto.pulumi.com 88 | resources: 89 | - workspaces/status 90 | verbs: 91 | - get 92 | - patch 93 | - update 94 | - apiGroups: 95 | - "" 96 | resources: 97 | - pods 98 | verbs: 99 | - create 100 | - delete 101 | - get 102 | - list 103 | - patch 104 | - update 105 | - watch 106 | - apiGroups: 107 | - "" 108 | resources: 109 | - services 110 | verbs: 111 | - create 112 | - delete 113 | - get 114 | - list 115 | - patch 116 | - update 117 | - watch 118 | - apiGroups: 119 | - pulumi.com 120 | resources: 121 | - programs 122 | verbs: 123 | - create 124 | - delete 125 | - get 126 | - list 127 | - patch 128 | - update 129 | - watch 130 | - apiGroups: 131 | - pulumi.com 132 | resources: 133 | - programs/finalizers 134 | verbs: 135 | - update 136 | - apiGroups: 137 | - pulumi.com 138 | resources: 139 | - programs/status 140 | verbs: 141 | - get 142 | - patch 143 | - update 144 | - apiGroups: 145 | - pulumi.com 146 | resources: 147 | - stacks 148 | verbs: 149 | - create 150 | - delete 151 | - get 152 | - list 153 | - patch 154 | - update 155 | - watch 156 | - apiGroups: 157 | - pulumi.com 158 | resources: 159 | - stacks/finalizers 160 | verbs: 161 | - update 162 | - apiGroups: 163 | - pulumi.com 164 | resources: 165 | - stacks/status 166 | verbs: 167 | - get 168 | - patch 169 | - update 170 | - apiGroups: 171 | - source.toolkit.fluxcd.io 172 | resources: 173 | - buckets 174 | verbs: 175 | - get 176 | - list 177 | - watch 178 | - apiGroups: 179 | - source.toolkit.fluxcd.io 180 | resources: 181 | - gitrepositories 182 | verbs: 183 | - get 184 | - list 185 | - watch 186 | - apiGroups: 187 | - source.toolkit.fluxcd.io 188 | resources: 189 | - ocirepositories 190 | verbs: 191 | - get 192 | - list 193 | - watch 194 | -------------------------------------------------------------------------------- /operator/config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: pulumi-kubernetes-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: pulumi-kubernetes-operator 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: pulumi-kubernetes-operator-controller-manager 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: pulumi-kubernetes-operator 16 | -------------------------------------------------------------------------------- /operator/config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: pulumi-kubernetes-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: pulumi-kubernetes-operator 9 | -------------------------------------------------------------------------------- /operator/config/rbac/token_request_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to call TokenRequest API to impersonate the controller-manager account. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: pulumi-kubernetes-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: token-request-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - serviceaccounts/token 14 | resourceNames: 15 | - controller-manager 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /operator/config/rbac/token_request_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: pulumi-kubernetes-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: token-request-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: token-request-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: pulumi-kubernetes-operator 16 | -------------------------------------------------------------------------------- /operator/config/rbac/view.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: pulumi-kubernetes-operator-view 5 | labels: 6 | app.kubernetes.io/name: pulumi-kubernetes-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 9 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 10 | rbac.authorization.k8s.io/aggregate-to-view: "true" 11 | rules: 12 | - apiGroups: 13 | - pulumi.com 14 | - auto.pulumi.com 15 | resources: ["*"] 16 | verbs: 17 | - get 18 | - list 19 | - watch -------------------------------------------------------------------------------- /operator/config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - pulumi_v1_program.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /operator/e2e/testdata/git-auth-nonroot/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: git-auth-nonroot 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: git-auth-nonroot 11 | namespace: git-auth-nonroot 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: git-auth-nonroot:system:auth-delegator 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: ClusterRole 20 | name: system:auth-delegator 21 | subjects: 22 | - kind: ServiceAccount 23 | name: git-auth-nonroot 24 | namespace: git-auth-nonroot 25 | --- 26 | apiVersion: v1 27 | kind: PersistentVolumeClaim 28 | metadata: 29 | name: state 30 | namespace: git-auth-nonroot 31 | spec: 32 | accessModes: 33 | - ReadWriteOnce 34 | resources: 35 | requests: 36 | storage: 100Mi 37 | --- 38 | apiVersion: v1 39 | kind: Secret 40 | metadata: 41 | name: git-auth 42 | namespace: git-auth-nonroot 43 | stringData: 44 | accessToken: $PULUMI_BOT_TOKEN 45 | --- 46 | apiVersion: pulumi.com/v1 47 | kind: Stack 48 | metadata: 49 | name: git-auth-nonroot 50 | namespace: git-auth-nonroot 51 | spec: 52 | projectRepo: https://github.com/pulumi/fixtures 53 | branch: main 54 | shallow: true 55 | repoDir: resources 56 | gitAuthSecret: git-auth 57 | 58 | stack: dev 59 | refresh: false 60 | continueResyncOnCommitMatch: false 61 | resyncFrequencySeconds: 60 62 | destroyOnFinalize: false 63 | 64 | # Enable file state for testing. 65 | envRefs: 66 | PULUMI_BACKEND_URL: 67 | type: Literal 68 | literal: 69 | value: "file:///state/" 70 | PULUMI_CONFIG_PASSPHRASE: 71 | type: Literal 72 | literal: 73 | value: "test" 74 | workspaceTemplate: 75 | spec: 76 | image: pulumi/pulumi:3.147.0-nonroot 77 | serviceAccountName: git-auth-nonroot 78 | podTemplate: 79 | spec: 80 | containers: 81 | - name: pulumi 82 | volumeMounts: 83 | - name: state 84 | mountPath: /state 85 | volumes: 86 | - name: state 87 | persistentVolumeClaim: 88 | claimName: state 89 | -------------------------------------------------------------------------------- /operator/e2e/testdata/issue-801/manifests.yaml: -------------------------------------------------------------------------------- 1 | # note: image is purposefully set to a non-existent image to simulate a rollout problem 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: issue-801 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: issue-801 11 | namespace: issue-801 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: issue-801:system:auth-delegator 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: ClusterRole 20 | name: system:auth-delegator 21 | subjects: 22 | - kind: ServiceAccount 23 | name: issue-801 24 | namespace: issue-801 25 | --- 26 | apiVersion: auto.pulumi.com/v1alpha1 27 | kind: Workspace 28 | metadata: 29 | name: issue-801 30 | namespace: issue-801 31 | spec: 32 | image: pulumi/nosuchimage:3.147.0-nonroot 33 | securityProfile: restricted 34 | serviceAccountName: issue-801 35 | git: 36 | url: https://github.com/pulumi/examples.git 37 | ref: 1e2fc471709448f3c9f7a250f28f1eafcde7017b 38 | dir: random-yaml 39 | shallow: true 40 | env: 41 | - name: PULUMI_CONFIG_PASSPHRASE 42 | value: test 43 | - name: PULUMI_BACKEND_URL 44 | value: file:///state/ 45 | 46 | podTemplate: 47 | spec: 48 | containers: 49 | - name: pulumi 50 | volumeMounts: 51 | - name: state 52 | mountPath: /state 53 | volumes: 54 | - name: state 55 | emptyDir: {} 56 | -------------------------------------------------------------------------------- /operator/e2e/testdata/issue-801/step2/manifests.yaml: -------------------------------------------------------------------------------- 1 | # modified: image tag 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: issue-801 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: issue-801 11 | namespace: issue-801 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: issue-801:system:auth-delegator 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: ClusterRole 20 | name: system:auth-delegator 21 | subjects: 22 | - kind: ServiceAccount 23 | name: issue-801 24 | namespace: issue-801 25 | --- 26 | apiVersion: auto.pulumi.com/v1alpha1 27 | kind: Workspace 28 | metadata: 29 | name: issue-801 30 | namespace: issue-801 31 | spec: 32 | image: pulumi/pulumi:3.147.0-nonroot 33 | securityProfile: restricted 34 | serviceAccountName: issue-801 35 | git: 36 | url: https://github.com/pulumi/examples.git 37 | ref: 1e2fc471709448f3c9f7a250f28f1eafcde7017b 38 | dir: random-yaml 39 | shallow: true 40 | env: 41 | - name: PULUMI_CONFIG_PASSPHRASE 42 | value: test 43 | - name: PULUMI_BACKEND_URL 44 | value: file:///state/ 45 | 46 | podTemplate: 47 | spec: 48 | containers: 49 | - name: pulumi 50 | volumeMounts: 51 | - name: state 52 | mountPath: /state 53 | volumes: 54 | - name: state 55 | emptyDir: {} 56 | -------------------------------------------------------------------------------- /operator/e2e/testdata/random-yaml-auth-error/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This NetworkPolicy allows ingress traffic to the source-controller pods 3 | # from specific namespaces and pods managed by pulumi-kubernetes-operator. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | name: allow-random-yaml-auth-error-fetch 8 | namespace: flux-system 9 | spec: 10 | podSelector: 11 | matchLabels: 12 | app: source-controller 13 | ingress: 14 | - ports: 15 | - protocol: TCP 16 | port: http 17 | from: 18 | - namespaceSelector: 19 | matchLabels: 20 | kubernetes.io/metadata.name: random-yaml-auth-error 21 | - podSelector: 22 | matchLabels: 23 | auto.pulumi.com/component: workspace 24 | auto.pulumi.com/workspace-name: random-yaml-auth-error 25 | policyTypes: 26 | - Ingress 27 | --- 28 | # Namespace to isolate the random-yaml-auth-error test. 29 | apiVersion: v1 30 | kind: Namespace 31 | metadata: 32 | name: random-yaml-auth-error 33 | --- 34 | # ServiceAccount for the random-yaml-auth-error namespace. 35 | # No permissions are granted to this service account. 36 | apiVersion: v1 37 | kind: ServiceAccount 38 | metadata: 39 | name: random-yaml-auth-error 40 | namespace: random-yaml-auth-error 41 | --- 42 | # Define a Flux Source GitRepository object for syncing Pulumi examples from a GitHub repository 43 | apiVersion: source.toolkit.fluxcd.io/v1 44 | kind: GitRepository 45 | metadata: 46 | name: pulumi-examples 47 | namespace: random-yaml-auth-error 48 | spec: 49 | interval: 10m 50 | ref: 51 | branch: master 52 | timeout: 60s 53 | url: https://github.com/pulumi/examples 54 | --- 55 | apiVersion: pulumi.com/v1 56 | kind: Stack 57 | metadata: 58 | name: random-yaml-auth-error 59 | namespace: random-yaml-auth-error 60 | spec: 61 | fluxSource: 62 | sourceRef: 63 | apiVersion: source.toolkit.fluxcd.io/v1 64 | kind: GitRepository 65 | name: pulumi-examples 66 | dir: random-yaml 67 | stack: dev 68 | refresh: false 69 | continueResyncOnCommitMatch: false 70 | resyncFrequencySeconds: 60 71 | destroyOnFinalize: false 72 | # Enable file state for testing. 73 | envRefs: 74 | PULUMI_BACKEND_URL: 75 | type: Literal 76 | literal: 77 | value: "file:///state/" 78 | PULUMI_CONFIG_PASSPHRASE: 79 | type: Literal 80 | literal: 81 | value: "test" 82 | workspaceTemplate: 83 | spec: 84 | image: pulumi/pulumi:3.147.0-nonroot 85 | serviceAccountName: random-yaml-auth-error 86 | podTemplate: 87 | spec: 88 | containers: 89 | - name: pulumi 90 | -------------------------------------------------------------------------------- /operator/e2e/testdata/random-yaml-nonroot/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: allow-random-yaml-nonroot-fetch 6 | namespace: flux-system 7 | spec: 8 | podSelector: 9 | matchLabels: 10 | app: source-controller 11 | ingress: 12 | - ports: 13 | - protocol: TCP 14 | port: http 15 | from: 16 | - namespaceSelector: 17 | matchLabels: 18 | kubernetes.io/metadata.name: random-yaml-nonroot 19 | - podSelector: 20 | matchLabels: 21 | auto.pulumi.com/component: workspace 22 | auto.pulumi.com/workspace-name: random-yaml-nonroot 23 | policyTypes: 24 | - Ingress 25 | --- 26 | apiVersion: v1 27 | kind: Namespace 28 | metadata: 29 | name: random-yaml-nonroot 30 | --- 31 | apiVersion: v1 32 | kind: ServiceAccount 33 | metadata: 34 | name: random-yaml-nonroot 35 | namespace: random-yaml-nonroot 36 | --- 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | kind: ClusterRoleBinding 39 | metadata: 40 | name: random-yaml-nonroot:system:auth-delegator 41 | roleRef: 42 | apiGroup: rbac.authorization.k8s.io 43 | kind: ClusterRole 44 | name: system:auth-delegator 45 | subjects: 46 | - kind: ServiceAccount 47 | name: random-yaml-nonroot 48 | namespace: random-yaml-nonroot 49 | --- 50 | apiVersion: v1 51 | kind: PersistentVolumeClaim 52 | metadata: 53 | name: state 54 | namespace: random-yaml-nonroot 55 | spec: 56 | accessModes: 57 | - ReadWriteOnce 58 | resources: 59 | requests: 60 | storage: 100Mi 61 | --- 62 | apiVersion: source.toolkit.fluxcd.io/v1 63 | kind: GitRepository 64 | metadata: 65 | name: pulumi-examples 66 | namespace: random-yaml-nonroot 67 | spec: 68 | interval: 10m 69 | ref: 70 | branch: master 71 | timeout: 60s 72 | url: https://github.com/pulumi/examples 73 | --- 74 | apiVersion: pulumi.com/v1 75 | kind: Stack 76 | metadata: 77 | name: random-yaml-nonroot 78 | namespace: random-yaml-nonroot 79 | spec: 80 | fluxSource: 81 | sourceRef: 82 | apiVersion: source.toolkit.fluxcd.io/v1 83 | kind: GitRepository 84 | name: pulumi-examples 85 | dir: random-yaml 86 | 87 | stack: dev 88 | refresh: false 89 | continueResyncOnCommitMatch: false 90 | resyncFrequencySeconds: 60 91 | destroyOnFinalize: false 92 | 93 | # Enable file state for testing. 94 | envRefs: 95 | PULUMI_BACKEND_URL: 96 | type: Literal 97 | literal: 98 | value: "file:///state/" 99 | PULUMI_CONFIG_PASSPHRASE: 100 | type: Literal 101 | literal: 102 | value: "test" 103 | workspaceTemplate: 104 | spec: 105 | image: pulumi/pulumi:3.147.0-nonroot 106 | serviceAccountName: random-yaml-nonroot 107 | podTemplate: 108 | spec: 109 | containers: 110 | - name: pulumi 111 | volumeMounts: 112 | - name: state 113 | mountPath: /state 114 | volumes: 115 | - name: state 116 | persistentVolumeClaim: 117 | claimName: state 118 | -------------------------------------------------------------------------------- /operator/e2e/testdata/targets/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: targets 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: targets 11 | namespace: targets 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: targets:system:auth-delegator 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: ClusterRole 20 | name: system:auth-delegator 21 | subjects: 22 | - kind: ServiceAccount 23 | name: targets 24 | namespace: targets 25 | --- 26 | apiVersion: v1 27 | kind: PersistentVolumeClaim 28 | metadata: 29 | name: state 30 | namespace: targets 31 | spec: 32 | accessModes: 33 | - ReadWriteOnce 34 | resources: 35 | requests: 36 | storage: 100Mi 37 | --- 38 | apiVersion: pulumi.com/v1 39 | kind: Program 40 | metadata: 41 | name: targets 42 | namespace: targets 43 | program: 44 | resources: 45 | notTargeted: 46 | type: random:RandomInteger 47 | properties: 48 | min: 1 49 | max: 100 50 | targeted: 51 | type: random:RandomInteger 52 | properties: 53 | min: 1 54 | max: 100 55 | outputs: 56 | targeted: ${targeted.result} 57 | notTargeted: ${notTargeted.result} # Should be absent 58 | --- 59 | apiVersion: pulumi.com/v1 60 | kind: Stack 61 | metadata: 62 | name: targets 63 | namespace: targets 64 | spec: 65 | stack: dev 66 | programRef: 67 | name: targets 68 | targets: 69 | - urn:pulumi:dev::targets::random:index/randomInteger:RandomInteger::targeted 70 | 71 | # Enable file state for testing. 72 | envRefs: 73 | PULUMI_BACKEND_URL: 74 | type: Literal 75 | literal: 76 | value: "file:///state/" 77 | PULUMI_CONFIG_PASSPHRASE: 78 | type: Literal 79 | literal: 80 | value: "test" 81 | workspaceTemplate: 82 | spec: 83 | image: pulumi/pulumi:3.147.0-nonroot 84 | serviceAccountName: targets 85 | podTemplate: 86 | spec: 87 | containers: 88 | - name: pulumi 89 | volumeMounts: 90 | - name: state 91 | mountPath: /state 92 | volumes: 93 | - name: state 94 | persistentVolumeClaim: 95 | claimName: state 96 | -------------------------------------------------------------------------------- /operator/hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/configitem.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // ConfigItemApplyConfiguration represents a declarative configuration of the ConfigItem type for use 20 | // with apply. 21 | type ConfigItemApplyConfiguration struct { 22 | Key *string `json:"key,omitempty"` 23 | Path *bool `json:"path,omitempty"` 24 | Value *string `json:"value,omitempty"` 25 | ValueFrom *ConfigValueFromApplyConfiguration `json:"valueFrom,omitempty"` 26 | Secret *bool `json:"secret,omitempty"` 27 | } 28 | 29 | // ConfigItemApplyConfiguration constructs a declarative configuration of the ConfigItem type for use with 30 | // apply. 31 | func ConfigItem() *ConfigItemApplyConfiguration { 32 | return &ConfigItemApplyConfiguration{} 33 | } 34 | 35 | // WithKey sets the Key field in the declarative configuration to the given value 36 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 37 | // If called multiple times, the Key field is set to the value of the last call. 38 | func (b *ConfigItemApplyConfiguration) WithKey(value string) *ConfigItemApplyConfiguration { 39 | b.Key = &value 40 | return b 41 | } 42 | 43 | // WithPath sets the Path field in the declarative configuration to the given value 44 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 45 | // If called multiple times, the Path field is set to the value of the last call. 46 | func (b *ConfigItemApplyConfiguration) WithPath(value bool) *ConfigItemApplyConfiguration { 47 | b.Path = &value 48 | return b 49 | } 50 | 51 | // WithValue sets the Value field in the declarative configuration to the given value 52 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 53 | // If called multiple times, the Value field is set to the value of the last call. 54 | func (b *ConfigItemApplyConfiguration) WithValue(value string) *ConfigItemApplyConfiguration { 55 | b.Value = &value 56 | return b 57 | } 58 | 59 | // WithValueFrom sets the ValueFrom field in the declarative configuration to the given value 60 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 61 | // If called multiple times, the ValueFrom field is set to the value of the last call. 62 | func (b *ConfigItemApplyConfiguration) WithValueFrom(value *ConfigValueFromApplyConfiguration) *ConfigItemApplyConfiguration { 63 | b.ValueFrom = value 64 | return b 65 | } 66 | 67 | // WithSecret sets the Secret field in the declarative configuration to the given value 68 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 69 | // If called multiple times, the Secret field is set to the value of the last call. 70 | func (b *ConfigItemApplyConfiguration) WithSecret(value bool) *ConfigItemApplyConfiguration { 71 | b.Secret = &value 72 | return b 73 | } 74 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/configvaluefrom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // ConfigValueFromApplyConfiguration represents a declarative configuration of the ConfigValueFrom type for use 20 | // with apply. 21 | type ConfigValueFromApplyConfiguration struct { 22 | Env *string `json:"env,omitempty"` 23 | Path *string `json:"path,omitempty"` 24 | } 25 | 26 | // ConfigValueFromApplyConfiguration constructs a declarative configuration of the ConfigValueFrom type for use with 27 | // apply. 28 | func ConfigValueFrom() *ConfigValueFromApplyConfiguration { 29 | return &ConfigValueFromApplyConfiguration{} 30 | } 31 | 32 | // WithEnv sets the Env field in the declarative configuration to the given value 33 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 34 | // If called multiple times, the Env field is set to the value of the last call. 35 | func (b *ConfigValueFromApplyConfiguration) WithEnv(value string) *ConfigValueFromApplyConfiguration { 36 | b.Env = &value 37 | return b 38 | } 39 | 40 | // WithPath sets the Path field in the declarative configuration to the given value 41 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 42 | // If called multiple times, the Path field is set to the value of the last call. 43 | func (b *ConfigValueFromApplyConfiguration) WithPath(value string) *ConfigValueFromApplyConfiguration { 44 | b.Path = &value 45 | return b 46 | } 47 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/embeddedobjectmeta.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // EmbeddedObjectMetaApplyConfiguration represents a declarative configuration of the EmbeddedObjectMeta type for use 20 | // with apply. 21 | type EmbeddedObjectMetaApplyConfiguration struct { 22 | Labels map[string]string `json:"labels,omitempty"` 23 | Annotations map[string]string `json:"annotations,omitempty"` 24 | } 25 | 26 | // EmbeddedObjectMetaApplyConfiguration constructs a declarative configuration of the EmbeddedObjectMeta type for use with 27 | // apply. 28 | func EmbeddedObjectMeta() *EmbeddedObjectMetaApplyConfiguration { 29 | return &EmbeddedObjectMetaApplyConfiguration{} 30 | } 31 | 32 | // WithLabels puts the entries into the Labels field in the declarative configuration 33 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 34 | // If called multiple times, the entries provided by each call will be put on the Labels field, 35 | // overwriting an existing map entries in Labels field with the same key. 36 | func (b *EmbeddedObjectMetaApplyConfiguration) WithLabels(entries map[string]string) *EmbeddedObjectMetaApplyConfiguration { 37 | if b.Labels == nil && len(entries) > 0 { 38 | b.Labels = make(map[string]string, len(entries)) 39 | } 40 | for k, v := range entries { 41 | b.Labels[k] = v 42 | } 43 | return b 44 | } 45 | 46 | // WithAnnotations puts the entries into the Annotations field in the declarative configuration 47 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 48 | // If called multiple times, the entries provided by each call will be put on the Annotations field, 49 | // overwriting an existing map entries in Annotations field with the same key. 50 | func (b *EmbeddedObjectMetaApplyConfiguration) WithAnnotations(entries map[string]string) *EmbeddedObjectMetaApplyConfiguration { 51 | if b.Annotations == nil && len(entries) > 0 { 52 | b.Annotations = make(map[string]string, len(entries)) 53 | } 54 | for k, v := range entries { 55 | b.Annotations[k] = v 56 | } 57 | return b 58 | } 59 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/embeddedpodtemplatespec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | ) 22 | 23 | // EmbeddedPodTemplateSpecApplyConfiguration represents a declarative configuration of the EmbeddedPodTemplateSpec type for use 24 | // with apply. 25 | type EmbeddedPodTemplateSpecApplyConfiguration struct { 26 | Metadata *EmbeddedObjectMetaApplyConfiguration `json:"metadata,omitempty"` 27 | Spec *v1.PodSpec `json:"spec,omitempty"` 28 | } 29 | 30 | // EmbeddedPodTemplateSpecApplyConfiguration constructs a declarative configuration of the EmbeddedPodTemplateSpec type for use with 31 | // apply. 32 | func EmbeddedPodTemplateSpec() *EmbeddedPodTemplateSpecApplyConfiguration { 33 | return &EmbeddedPodTemplateSpecApplyConfiguration{} 34 | } 35 | 36 | // WithMetadata sets the Metadata field in the declarative configuration to the given value 37 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 38 | // If called multiple times, the Metadata field is set to the value of the last call. 39 | func (b *EmbeddedPodTemplateSpecApplyConfiguration) WithMetadata(value *EmbeddedObjectMetaApplyConfiguration) *EmbeddedPodTemplateSpecApplyConfiguration { 40 | b.Metadata = value 41 | return b 42 | } 43 | 44 | // WithSpec sets the Spec field in the declarative configuration to the given value 45 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 46 | // If called multiple times, the Spec field is set to the value of the last call. 47 | func (b *EmbeddedPodTemplateSpecApplyConfiguration) WithSpec(value v1.PodSpec) *EmbeddedPodTemplateSpecApplyConfiguration { 48 | b.Spec = &value 49 | return b 50 | } 51 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/fluxsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // FluxSourceApplyConfiguration represents a declarative configuration of the FluxSource type for use 20 | // with apply. 21 | type FluxSourceApplyConfiguration struct { 22 | Url *string `json:"url,omitempty"` 23 | Digest *string `json:"digest,omitempty"` 24 | Dir *string `json:"dir,omitempty"` 25 | } 26 | 27 | // FluxSourceApplyConfiguration constructs a declarative configuration of the FluxSource type for use with 28 | // apply. 29 | func FluxSource() *FluxSourceApplyConfiguration { 30 | return &FluxSourceApplyConfiguration{} 31 | } 32 | 33 | // WithUrl sets the Url field in the declarative configuration to the given value 34 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 35 | // If called multiple times, the Url field is set to the value of the last call. 36 | func (b *FluxSourceApplyConfiguration) WithUrl(value string) *FluxSourceApplyConfiguration { 37 | b.Url = &value 38 | return b 39 | } 40 | 41 | // WithDigest sets the Digest field in the declarative configuration to the given value 42 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 43 | // If called multiple times, the Digest field is set to the value of the last call. 44 | func (b *FluxSourceApplyConfiguration) WithDigest(value string) *FluxSourceApplyConfiguration { 45 | b.Digest = &value 46 | return b 47 | } 48 | 49 | // WithDir sets the Dir field in the declarative configuration to the given value 50 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 51 | // If called multiple times, the Dir field is set to the value of the last call. 52 | func (b *FluxSourceApplyConfiguration) WithDir(value string) *FluxSourceApplyConfiguration { 53 | b.Dir = &value 54 | return b 55 | } 56 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/gitauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | ) 22 | 23 | // GitAuthApplyConfiguration represents a declarative configuration of the GitAuth type for use 24 | // with apply. 25 | type GitAuthApplyConfiguration struct { 26 | SSHPrivateKey *v1.SecretKeySelector `json:"sshPrivateKey,omitempty"` 27 | Username *v1.SecretKeySelector `json:"username,omitempty"` 28 | Password *v1.SecretKeySelector `json:"password,omitempty"` 29 | Token *v1.SecretKeySelector `json:"token,omitempty"` 30 | } 31 | 32 | // GitAuthApplyConfiguration constructs a declarative configuration of the GitAuth type for use with 33 | // apply. 34 | func GitAuth() *GitAuthApplyConfiguration { 35 | return &GitAuthApplyConfiguration{} 36 | } 37 | 38 | // WithSSHPrivateKey sets the SSHPrivateKey field in the declarative configuration to the given value 39 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 40 | // If called multiple times, the SSHPrivateKey field is set to the value of the last call. 41 | func (b *GitAuthApplyConfiguration) WithSSHPrivateKey(value v1.SecretKeySelector) *GitAuthApplyConfiguration { 42 | b.SSHPrivateKey = &value 43 | return b 44 | } 45 | 46 | // WithUsername sets the Username field in the declarative configuration to the given value 47 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 48 | // If called multiple times, the Username field is set to the value of the last call. 49 | func (b *GitAuthApplyConfiguration) WithUsername(value v1.SecretKeySelector) *GitAuthApplyConfiguration { 50 | b.Username = &value 51 | return b 52 | } 53 | 54 | // WithPassword sets the Password field in the declarative configuration to the given value 55 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 56 | // If called multiple times, the Password field is set to the value of the last call. 57 | func (b *GitAuthApplyConfiguration) WithPassword(value v1.SecretKeySelector) *GitAuthApplyConfiguration { 58 | b.Password = &value 59 | return b 60 | } 61 | 62 | // WithToken sets the Token field in the declarative configuration to the given value 63 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 64 | // If called multiple times, the Token field is set to the value of the last call. 65 | func (b *GitAuthApplyConfiguration) WithToken(value v1.SecretKeySelector) *GitAuthApplyConfiguration { 66 | b.Token = &value 67 | return b 68 | } 69 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/gitsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // GitSourceApplyConfiguration represents a declarative configuration of the GitSource type for use 20 | // with apply. 21 | type GitSourceApplyConfiguration struct { 22 | URL *string `json:"url,omitempty"` 23 | Ref *string `json:"ref,omitempty"` 24 | Dir *string `json:"dir,omitempty"` 25 | Auth *GitAuthApplyConfiguration `json:"auth,omitempty"` 26 | Shallow *bool `json:"shallow,omitempty"` 27 | } 28 | 29 | // GitSourceApplyConfiguration constructs a declarative configuration of the GitSource type for use with 30 | // apply. 31 | func GitSource() *GitSourceApplyConfiguration { 32 | return &GitSourceApplyConfiguration{} 33 | } 34 | 35 | // WithURL sets the URL field in the declarative configuration to the given value 36 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 37 | // If called multiple times, the URL field is set to the value of the last call. 38 | func (b *GitSourceApplyConfiguration) WithURL(value string) *GitSourceApplyConfiguration { 39 | b.URL = &value 40 | return b 41 | } 42 | 43 | // WithRef sets the Ref field in the declarative configuration to the given value 44 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 45 | // If called multiple times, the Ref field is set to the value of the last call. 46 | func (b *GitSourceApplyConfiguration) WithRef(value string) *GitSourceApplyConfiguration { 47 | b.Ref = &value 48 | return b 49 | } 50 | 51 | // WithDir sets the Dir field in the declarative configuration to the given value 52 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 53 | // If called multiple times, the Dir field is set to the value of the last call. 54 | func (b *GitSourceApplyConfiguration) WithDir(value string) *GitSourceApplyConfiguration { 55 | b.Dir = &value 56 | return b 57 | } 58 | 59 | // WithAuth sets the Auth field in the declarative configuration to the given value 60 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 61 | // If called multiple times, the Auth field is set to the value of the last call. 62 | func (b *GitSourceApplyConfiguration) WithAuth(value *GitAuthApplyConfiguration) *GitSourceApplyConfiguration { 63 | b.Auth = value 64 | return b 65 | } 66 | 67 | // WithShallow sets the Shallow field in the declarative configuration to the given value 68 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 69 | // If called multiple times, the Shallow field is set to the value of the last call. 70 | func (b *GitSourceApplyConfiguration) WithShallow(value bool) *GitSourceApplyConfiguration { 71 | b.Shallow = &value 72 | return b 73 | } 74 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/localsource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | // LocalSourceApplyConfiguration represents a declarative configuration of the LocalSource type for use 20 | // with apply. 21 | type LocalSourceApplyConfiguration struct { 22 | Dir *string `json:"dir,omitempty"` 23 | } 24 | 25 | // LocalSourceApplyConfiguration constructs a declarative configuration of the LocalSource type for use with 26 | // apply. 27 | func LocalSource() *LocalSourceApplyConfiguration { 28 | return &LocalSourceApplyConfiguration{} 29 | } 30 | 31 | // WithDir sets the Dir field in the declarative configuration to the given value 32 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 33 | // If called multiple times, the Dir field is set to the value of the last call. 34 | func (b *LocalSourceApplyConfiguration) WithDir(value string) *LocalSourceApplyConfiguration { 35 | b.Dir = &value 36 | return b 37 | } 38 | -------------------------------------------------------------------------------- /operator/internal/apply/auto/v1alpha1/workspacestatus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | v1 "k8s.io/client-go/applyconfigurations/meta/v1" 21 | ) 22 | 23 | // WorkspaceStatusApplyConfiguration represents a declarative configuration of the WorkspaceStatus type for use 24 | // with apply. 25 | type WorkspaceStatusApplyConfiguration struct { 26 | ObservedGeneration *int64 `json:"observedGeneration,omitempty"` 27 | Address *string `json:"address,omitempty"` 28 | Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` 29 | } 30 | 31 | // WorkspaceStatusApplyConfiguration constructs a declarative configuration of the WorkspaceStatus type for use with 32 | // apply. 33 | func WorkspaceStatus() *WorkspaceStatusApplyConfiguration { 34 | return &WorkspaceStatusApplyConfiguration{} 35 | } 36 | 37 | // WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value 38 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 39 | // If called multiple times, the ObservedGeneration field is set to the value of the last call. 40 | func (b *WorkspaceStatusApplyConfiguration) WithObservedGeneration(value int64) *WorkspaceStatusApplyConfiguration { 41 | b.ObservedGeneration = &value 42 | return b 43 | } 44 | 45 | // WithAddress sets the Address field in the declarative configuration to the given value 46 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 47 | // If called multiple times, the Address field is set to the value of the last call. 48 | func (b *WorkspaceStatusApplyConfiguration) WithAddress(value string) *WorkspaceStatusApplyConfiguration { 49 | b.Address = &value 50 | return b 51 | } 52 | 53 | // WithConditions adds the given value to the Conditions field in the declarative configuration 54 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 55 | // If called multiple times, values provided by each call will be appended to the Conditions field. 56 | func (b *WorkspaceStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *WorkspaceStatusApplyConfiguration { 57 | for i := range values { 58 | if values[i] == nil { 59 | panic("nil value passed to WithConditions") 60 | } 61 | b.Conditions = append(b.Conditions, *values[i]) 62 | } 63 | return b 64 | } 65 | -------------------------------------------------------------------------------- /operator/internal/apply/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package internal 18 | 19 | import ( 20 | fmt "fmt" 21 | sync "sync" 22 | 23 | typed "sigs.k8s.io/structured-merge-diff/v4/typed" 24 | ) 25 | 26 | func Parser() *typed.Parser { 27 | parserOnce.Do(func() { 28 | var err error 29 | parser, err = typed.NewParser(schemaYAML) 30 | if err != nil { 31 | panic(fmt.Sprintf("Failed to parse schema: %v", err)) 32 | } 33 | }) 34 | return parser 35 | } 36 | 37 | var parserOnce sync.Once 38 | var parser *typed.Parser 39 | var schemaYAML = typed.YAMLObject(`types: 40 | - name: __untyped_atomic_ 41 | scalar: untyped 42 | list: 43 | elementType: 44 | namedType: __untyped_atomic_ 45 | elementRelationship: atomic 46 | map: 47 | elementType: 48 | namedType: __untyped_atomic_ 49 | elementRelationship: atomic 50 | - name: __untyped_deduced_ 51 | scalar: untyped 52 | list: 53 | elementType: 54 | namedType: __untyped_atomic_ 55 | elementRelationship: atomic 56 | map: 57 | elementType: 58 | namedType: __untyped_deduced_ 59 | elementRelationship: separable 60 | `) 61 | -------------------------------------------------------------------------------- /operator/internal/apply/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 16 | 17 | package apply 18 | 19 | import ( 20 | v1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" 21 | autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/internal/apply/auto/v1alpha1" 22 | internal "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/internal/apply/internal" 23 | runtime "k8s.io/apimachinery/pkg/runtime" 24 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | testing "k8s.io/client-go/testing" 26 | ) 27 | 28 | // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no 29 | // apply configuration type exists for the given GroupVersionKind. 30 | func ForKind(kind schema.GroupVersionKind) interface{} { 31 | switch kind { 32 | // Group=auto.pulumi.com, Version=v1alpha1 33 | case v1alpha1.SchemeGroupVersion.WithKind("ConfigItem"): 34 | return &autov1alpha1.ConfigItemApplyConfiguration{} 35 | case v1alpha1.SchemeGroupVersion.WithKind("ConfigValueFrom"): 36 | return &autov1alpha1.ConfigValueFromApplyConfiguration{} 37 | case v1alpha1.SchemeGroupVersion.WithKind("EmbeddedObjectMeta"): 38 | return &autov1alpha1.EmbeddedObjectMetaApplyConfiguration{} 39 | case v1alpha1.SchemeGroupVersion.WithKind("EmbeddedPodTemplateSpec"): 40 | return &autov1alpha1.EmbeddedPodTemplateSpecApplyConfiguration{} 41 | case v1alpha1.SchemeGroupVersion.WithKind("FluxSource"): 42 | return &autov1alpha1.FluxSourceApplyConfiguration{} 43 | case v1alpha1.SchemeGroupVersion.WithKind("GitAuth"): 44 | return &autov1alpha1.GitAuthApplyConfiguration{} 45 | case v1alpha1.SchemeGroupVersion.WithKind("GitSource"): 46 | return &autov1alpha1.GitSourceApplyConfiguration{} 47 | case v1alpha1.SchemeGroupVersion.WithKind("LocalSource"): 48 | return &autov1alpha1.LocalSourceApplyConfiguration{} 49 | case v1alpha1.SchemeGroupVersion.WithKind("Update"): 50 | return &autov1alpha1.UpdateApplyConfiguration{} 51 | case v1alpha1.SchemeGroupVersion.WithKind("UpdateSpec"): 52 | return &autov1alpha1.UpdateSpecApplyConfiguration{} 53 | case v1alpha1.SchemeGroupVersion.WithKind("UpdateStatus"): 54 | return &autov1alpha1.UpdateStatusApplyConfiguration{} 55 | case v1alpha1.SchemeGroupVersion.WithKind("Workspace"): 56 | return &autov1alpha1.WorkspaceApplyConfiguration{} 57 | case v1alpha1.SchemeGroupVersion.WithKind("WorkspaceSpec"): 58 | return &autov1alpha1.WorkspaceSpecApplyConfiguration{} 59 | case v1alpha1.SchemeGroupVersion.WithKind("WorkspaceStack"): 60 | return &autov1alpha1.WorkspaceStackApplyConfiguration{} 61 | case v1alpha1.SchemeGroupVersion.WithKind("WorkspaceStatus"): 62 | return &autov1alpha1.WorkspaceStatusApplyConfiguration{} 63 | 64 | } 65 | return nil 66 | } 67 | 68 | func NewTypeConverter(scheme *runtime.Scheme) *testing.TypeConverter { 69 | return &testing.TypeConverter{Scheme: scheme, TypeResolver: internal.Parser()} 70 | } 71 | -------------------------------------------------------------------------------- /operator/internal/controller/auto/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controller 16 | 17 | type ComponentType = string 18 | 19 | const ( 20 | ComponentLabel = "auto.pulumi.com/component" 21 | WorkspaceNameLabel = "auto.pulumi.com/workspace-name" 22 | UpdateNameLabel = "auto.pulumi.com/update-name" 23 | 24 | WorkspaceComponent ComponentType = "workspace" 25 | UpdateComponent ComponentType = "update" 26 | ) 27 | -------------------------------------------------------------------------------- /operator/internal/controller/auto/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controller 16 | 17 | import ( 18 | "fmt" 19 | "path/filepath" 20 | "runtime" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "k8s.io/client-go/rest" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | logf "sigs.k8s.io/controller-runtime/pkg/log" 30 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 31 | 32 | autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" 33 | //+kubebuilder:scaffold:imports 34 | ) 35 | 36 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 37 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 38 | 39 | var cfg *rest.Config 40 | var k8sClient client.Client 41 | var testEnv *envtest.Environment 42 | 43 | func TestControllers(t *testing.T) { 44 | RegisterFailHandler(Fail) 45 | 46 | RunSpecs(t, "Controller Suite") 47 | } 48 | 49 | var _ = BeforeSuite(func() { 50 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 51 | 52 | By("bootstrapping test environment") 53 | testEnv = &envtest.Environment{ 54 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, 55 | ErrorIfCRDPathMissing: true, 56 | 57 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 58 | // without call the makefile target test. If not informed it will look for the 59 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 60 | // Note that you must have the required binaries setup under the bin directory to perform 61 | // the tests directly. When we run make test it will be setup and used automatically. 62 | BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", 63 | fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), 64 | } 65 | 66 | var err error 67 | // cfg is defined in this file globally. 68 | cfg, err = testEnv.Start() 69 | Expect(err).NotTo(HaveOccurred()) 70 | Expect(cfg).NotTo(BeNil()) 71 | 72 | GinkgoT().Logf("Test environment started: --server=%s --insecure-skip-tls-verify=true", cfg.Host) 73 | 74 | err = autov1alpha1.AddToScheme(scheme.Scheme) 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | //+kubebuilder:scaffold:scheme 78 | 79 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 80 | Expect(err).NotTo(HaveOccurred()) 81 | Expect(k8sClient).NotTo(BeNil()) 82 | 83 | }) 84 | 85 | var _ = AfterSuite(func() { 86 | By("tearing down the test environment") 87 | err := testEnv.Stop() 88 | Expect(err).NotTo(HaveOccurred()) 89 | }) 90 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | type ComponentType = string 18 | 19 | const ( 20 | ComponentLabel = "pulumi.com/component" 21 | StackNameLabel = "pulumi.com/stack-name" 22 | 23 | StackComponent ComponentType = "stack" 24 | ) 25 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/flux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" 22 | "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/shared" 23 | pulumiv1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/runtime/schema" 27 | "sigs.k8s.io/controller-runtime/pkg/event" 28 | "sigs.k8s.io/controller-runtime/pkg/predicate" 29 | ) 30 | 31 | func (sess *stackReconcilerSession) SetupWorkspaceFromFluxSource(ctx context.Context, source unstructured.Unstructured, 32 | artifact pulumiv1.Artifact, dir string) error { 33 | // this source artifact fetching code is based closely on 34 | sess.logger.V(1).Info("Setting up pulumi workspace for stack", "stack", sess.stack, "workspace", sess.ws.Name, 35 | "artifact", artifact, "dir", dir) 36 | 37 | sess.ws.Spec.Flux = &autov1alpha1.FluxSource{ 38 | Url: artifact.URL, 39 | Digest: artifact.Digest, 40 | Dir: dir, 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func getSourceGVK(src shared.FluxSourceReference) (schema.GroupVersionKind, error) { 47 | gv, err := schema.ParseGroupVersion(src.APIVersion) 48 | return gv.WithKind(src.Kind), err 49 | } 50 | 51 | func fluxSourceKey(gvk schema.GroupVersionKind, name string) string { 52 | return fmt.Sprintf("%s:%s", gvk, name) 53 | } 54 | 55 | type SourceRevisionChangePredicate struct { 56 | predicate.Funcs 57 | } 58 | 59 | func (SourceRevisionChangePredicate) Update(e event.UpdateEvent) bool { 60 | if e.ObjectOld == nil || e.ObjectNew == nil { 61 | return false 62 | } 63 | 64 | oldSource, ok := e.ObjectOld.(*unstructured.Unstructured) 65 | if !ok || oldSource == nil { 66 | return false 67 | } 68 | oldArtifact, _, _ := getArtifact(*oldSource) 69 | 70 | newSource, ok := e.ObjectNew.(*unstructured.Unstructured) 71 | if !ok || newSource == nil { 72 | return false 73 | } 74 | newArtifact, _, _ := getArtifact(*newSource) 75 | 76 | if oldArtifact == nil && newArtifact != nil { 77 | return true 78 | } 79 | 80 | if oldArtifact != nil && newArtifact != nil && 81 | !oldArtifact.HasRevision(newArtifact.Revision) { 82 | return true 83 | } 84 | 85 | return false 86 | } 87 | 88 | // getArtifact retrieves the Artifact from the given Source object. 89 | // It returns the Artifact, a boolean indicating if the Artifact was found, 90 | // and an error if there was an issue retrieving or decoding the Artifact. 91 | func getArtifact(source unstructured.Unstructured) (*pulumiv1.Artifact, bool, error) { 92 | m, hasArtifact, err := unstructured.NestedMap(source.Object, "status", "artifact") 93 | if err != nil { 94 | return nil, false, err 95 | } 96 | if !hasArtifact { 97 | return nil, false, nil 98 | } 99 | var artifact pulumiv1.Artifact 100 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, &artifact); err != nil { 101 | return nil, false, fmt.Errorf("failed to decode artifact: %w", err) 102 | } 103 | return &artifact, true, nil 104 | } 105 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/git_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/shared" 21 | "github.com/pulumi/pulumi/sdk/v3/go/auto" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | const _pko = "https://github.com/pulumi/pulumi-kubernetes-operator.git" 27 | 28 | func TestValidation(t *testing.T) { 29 | tests := []struct { 30 | name string 31 | source shared.GitSource 32 | wantErr string 33 | }{ 34 | { 35 | name: "missing projectRepo", 36 | source: shared.GitSource{ 37 | Branch: "$$$", 38 | }, 39 | wantErr: `missing "projectRepo"`, 40 | }, 41 | { 42 | name: "missing branch and commit", 43 | source: shared.GitSource{ 44 | ProjectRepo: _pko, 45 | }, 46 | wantErr: `missing "commit" or "branch"`, 47 | }, 48 | { 49 | name: "branch and commit", 50 | source: shared.GitSource{ 51 | ProjectRepo: _pko, 52 | Branch: "master", 53 | Commit: "55d7ace59a14b8ac7d4def0065040f1c31c90cd3", 54 | }, 55 | wantErr: `only one of "commit" or "branch"`, 56 | }, 57 | { 58 | name: "invalid url", 59 | source: shared.GitSource{ 60 | ProjectRepo: "hhtp://github.com/pulumi", 61 | Branch: "master", 62 | }, 63 | wantErr: "invalid URL scheme: hhtp", 64 | }, 65 | } 66 | 67 | for _, tt := range tests { 68 | _, err := NewGitSource(tt.source, nil /* auth */) 69 | assert.ErrorContains(t, err, tt.wantErr) 70 | 71 | } 72 | } 73 | 74 | func TestCurrentCommit(t *testing.T) { 75 | t.Parallel() 76 | 77 | tests := []struct { 78 | name string 79 | source shared.GitSource 80 | auth *auto.GitAuth 81 | wantErr string 82 | want string 83 | eq assert.ValueAssertionFunc 84 | }{ 85 | { 86 | name: "branch", 87 | source: shared.GitSource{ 88 | ProjectRepo: "https://github.com/git-fixtures/basic.git", 89 | Branch: "master", 90 | }, 91 | want: "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", 92 | }, 93 | { 94 | name: "tag", 95 | source: shared.GitSource{ 96 | ProjectRepo: _pko, 97 | Branch: "refs/tags/v1.16.0", 98 | }, 99 | want: "2ca775387e522fd5c29668a85bfba2f8fd791848", 100 | }, 101 | { 102 | name: "commit", 103 | source: shared.GitSource{ 104 | ProjectRepo: _pko, 105 | Commit: "55d7ace59a14b8ac7d4def0065040f1c31c90cd3", 106 | }, 107 | want: "55d7ace59a14b8ac7d4def0065040f1c31c90cd3", 108 | }, 109 | { 110 | name: "non-existent ref", 111 | source: shared.GitSource{ 112 | ProjectRepo: _pko, 113 | Branch: "doesntexist", 114 | }, 115 | wantErr: "no commits found for ref", 116 | }, 117 | } 118 | 119 | for _, tt := range tests { 120 | t.Run(tt.name, func(t *testing.T) { 121 | gs, err := NewGitSource(tt.source, nil /* auth */) 122 | require.NoError(t, err) 123 | 124 | commit, err := gs.CurrentCommit(t.Context()) 125 | if tt.wantErr != "" { 126 | assert.ErrorContains(t, err, tt.wantErr) 127 | return 128 | } 129 | 130 | assert.Equal(t, tt.want, commit) 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/metrics_program.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | "github.com/prometheus/client_golang/prometheus" 19 | "sigs.k8s.io/controller-runtime/pkg/metrics" 20 | ) 21 | 22 | var ( 23 | numPrograms = prometheus.NewGauge(prometheus.GaugeOpts{ 24 | Name: "programs_active", 25 | Help: "Number of Program objects currently tracked by the Pulumi Kubernetes Operator", 26 | }) 27 | ) 28 | 29 | // init registers Program custom metrics with the global prometheus registry on startup. 30 | func init() { 31 | metrics.Registry.MustRegister(numPrograms) 32 | } 33 | 34 | // newProgramCallback is a callback that is called when a new Program object is created. 35 | func newProgramCallback(_ any) { 36 | numPrograms.Inc() 37 | } 38 | 39 | // updateProgramCallback is a callback that is called when a Program object is updated. 40 | func deleteProgramCallback(_ any) { 41 | numPrograms.Dec() 42 | 43 | val, err := getGaugeValue(numPrograms) 44 | if err == nil && val < 0 { 45 | numPrograms.Set(0) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/metrics_program_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | "github.com/prometheus/client_golang/prometheus/testutil" 21 | ) 22 | 23 | var _ = Describe("Program Metrics", Ordered, func() { 24 | BeforeAll(func() { 25 | numPrograms.Set(0) 26 | }) 27 | 28 | It("should increment the programs_active metric when a new Program is created", func() { 29 | newProgramCallback(nil) 30 | Expect(testutil.ToFloat64(numPrograms)).To(Equal(1.0)) 31 | }) 32 | 33 | It("should increment the programs_active metric when another Program is created", func() { 34 | newProgramCallback(nil) 35 | Expect(testutil.ToFloat64(numPrograms)).To(Equal(2.0)) 36 | }) 37 | 38 | It("should decrement the programs_active metric when a Program is deleted", func() { 39 | deleteProgramCallback(nil) 40 | Expect(testutil.ToFloat64(numPrograms)).To(Equal(1.0)) 41 | }) 42 | 43 | It("should decrement the programs_active metric when another Program is deleted", func() { 44 | deleteProgramCallback(nil) 45 | Expect(testutil.ToFloat64(numPrograms)).To(Equal(0.0)) 46 | }) 47 | 48 | It("should not decrement the programs_active metric when a Program is deleted and the metric is already at 0", func() { 49 | deleteProgramCallback(nil) 50 | Expect(testutil.ToFloat64(numPrograms)).To(Equal(0.0)) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/migration.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/shared" 19 | pulumiv1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/v1" 20 | apierrors "k8s.io/apimachinery/pkg/api/errors" 21 | "k8s.io/apimachinery/pkg/util/validation/field" 22 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 23 | ) 24 | 25 | func ValidateStack(s *pulumiv1.Stack) (admission.Warnings, error) { 26 | var allErrs field.ErrorList 27 | 28 | if s.Spec.ExpectNoRefreshChanges { 29 | field.Invalid(field.NewPath("spec", "expectNoRefreshChanges"), s.Spec.ExpectNoRefreshChanges, "expectNoRefreshChanges is ignored") 30 | } 31 | 32 | // obsolete: EnvRef containing a reference to a file or environment variable 33 | for key, envRef := range s.Spec.EnvRefs { 34 | path := field.NewPath("spec", "envRefs").Key(key) 35 | if envRef.SelectorType != shared.ResourceSelectorLiteral && envRef.SelectorType != shared.ResourceSelectorSecret { 36 | field.NotSupported(path.Child("selectorType"), envRef.SelectorType, 37 | []string{string(shared.ResourceSelectorLiteral), string(shared.ResourceSelectorSecret)}) 38 | } 39 | if envRef.SelectorType == shared.ResourceSelectorSecret && envRef.SecretRef != nil && envRef.SecretRef.Namespace != "" && envRef.SecretRef.Namespace != s.Namespace { 40 | field.Invalid(path.Child("secret", "namespace"), envRef.SecretRef.Namespace, "cross-namespace references are not allowed") 41 | } 42 | } 43 | 44 | if len(allErrs) == 0 { 45 | return nil, nil 46 | } 47 | return nil, apierrors.NewInvalid( 48 | s.GroupVersionKind().GroupKind(), 49 | s.Name, allErrs) 50 | } 51 | -------------------------------------------------------------------------------- /operator/internal/controller/pulumi/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pulumi 16 | 17 | import ( 18 | "github.com/prometheus/client_golang/prometheus" 19 | dto "github.com/prometheus/client_model/go" 20 | pulumiv1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/pulumi/v1" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/client-go/tools/record" 23 | ) 24 | 25 | func exactlyOneOf(these ...bool) bool { 26 | var found bool 27 | for _, b := range these { 28 | if found && b { 29 | return false 30 | } 31 | found = found || b 32 | } 33 | return found 34 | } 35 | 36 | // getGaugeValue returns the value of a gauge metric. This is useful to check that a gauge 37 | // does not go into negative values. 38 | func getGaugeValue(metric prometheus.Gauge) (float64, error) { 39 | var m = &dto.Metric{} 40 | if err := metric.Write(m); err != nil { 41 | return 0, err 42 | } 43 | return m.Gauge.GetValue(), nil 44 | } 45 | 46 | func emitEvent(recorder record.EventRecorder, object runtime.Object, event pulumiv1.StackEvent, messageFmt string, args ...interface{}) { 47 | recorder.Eventf(object, event.EventType(), event.Reason(), messageFmt, args...) 48 | } 49 | -------------------------------------------------------------------------------- /operator/internal/webhook/auto/v1alpha1/workspace_webhook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | corev1 "k8s.io/api/core/v1" 22 | "k8s.io/apimachinery/pkg/api/resource" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | "sigs.k8s.io/controller-runtime/pkg/webhook" 25 | 26 | autov1alpha1 "github.com/pulumi/pulumi-kubernetes-operator/v2/operator/api/auto/v1alpha1" 27 | ) 28 | 29 | const ( 30 | // SecurityProfileBaselineDefaultImage is the default image used when the security profile is 'baseline'. 31 | SecurityProfileBaselineDefaultImage = "pulumi/pulumi:latest" 32 | // SecurityProfileRestrictedDefaultImage is the default image used when the security profile is 'restricted'. 33 | SecurityProfileRestrictedDefaultImage = "pulumi/pulumi:latest-nonroot" 34 | ) 35 | 36 | // // SetupWorkspaceWebhookWithManager registers the webhook for Workspace in the manager. 37 | // func SetupWorkspaceWebhookWithManager(mgr ctrl.Manager) error { 38 | // return ctrl.NewWebhookManagedBy(mgr).For(&autov1alpha1.Workspace{}). 39 | // WithDefaulter(&WorkspaceCustomDefaulter{}). 40 | // Complete() 41 | // } 42 | 43 | //// +kubebuilder:webhook:path=/mutate-auto-pulumi-com-v1alpha1-workspace,mutating=true,failurePolicy=fail,sideEffects=None,groups=auto.pulumi.com,resources=workspaces,verbs=create;update,versions=v1alpha1,name=mworkspace-v1alpha1.kb.io,admissionReviewVersions=v1 44 | 45 | // WorkspaceCustomDefaulter struct is responsible for setting default values on the custom resource of the 46 | // Kind Workspace when those are created or updated. 47 | // 48 | // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, 49 | // as it is used only for temporary operations and does not need to be deeply copied. 50 | type WorkspaceCustomDefaulter struct { 51 | // TODO(user): Add more fields as needed for defaulting 52 | } 53 | 54 | var _ webhook.CustomDefaulter = &WorkspaceCustomDefaulter{} 55 | 56 | // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Workspace. 57 | func (d *WorkspaceCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { 58 | w, ok := obj.(*autov1alpha1.Workspace) 59 | if !ok { 60 | return fmt.Errorf("expected an Workspace object but got %T", obj) 61 | } 62 | 63 | if w.Spec.SecurityProfile == "" { 64 | w.Spec.SecurityProfile = autov1alpha1.SecurityProfileRestricted 65 | } 66 | 67 | if w.Spec.Image == "" { 68 | switch w.Spec.SecurityProfile { 69 | case autov1alpha1.SecurityProfileRestricted: 70 | w.Spec.Image = SecurityProfileRestrictedDefaultImage 71 | case autov1alpha1.SecurityProfileBaseline: 72 | w.Spec.Image = SecurityProfileBaselineDefaultImage 73 | } 74 | } 75 | 76 | // default resource requirements here are designed to provide a "burstable" workspace. 77 | if w.Spec.Resources.Requests == nil { 78 | w.Spec.Resources.Requests = corev1.ResourceList{} 79 | } 80 | if w.Spec.Resources.Requests.Memory().IsZero() { 81 | w.Spec.Resources.Requests[corev1.ResourceMemory] = resource.MustParse("64Mi") 82 | } 83 | if w.Spec.Resources.Requests.Cpu().IsZero() { 84 | w.Spec.Resources.Requests[corev1.ResourceCPU] = resource.MustParse("100m") 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /operator/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | var Version string = "v2.1.0" 18 | -------------------------------------------------------------------------------- /scripts/ci-infra-create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o nounset -o errexit -o pipefail 3 | 4 | echo Creating S3 backend and KMS key 5 | 6 | pushd test/s3backend 7 | yarn install --json --verbose >out 8 | tail -10 out 9 | pulumi stack init "${STACK}" 10 | pulumi up --skip-preview --yes 11 | touch ~/.envfile 12 | echo export PULUMI_S3_BACKEND_BUCKET="`pulumi stack output bucketName`" > ~/.envfile 13 | echo export PULUMI_KMS_KEY="`pulumi stack output kmsKey`" >> ~/.envfile 14 | popd 15 | -------------------------------------------------------------------------------- /scripts/ci-infra-destroy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o nounset -o errexit -o pipefail 3 | 4 | echo Deleting S3 backend and KMS Key... 5 | pushd test/s3backend 6 | 7 | pulumi stack select "${STACK}" 8 | bucket=`pulumi stack output bucketName` 9 | echo Deleting contents in S3 bucket $bucket... 10 | aws s3 rm s3://${bucket} --recursive 11 | 12 | echo Destroying stack 13 | pulumi destroy --skip-preview --yes && \ 14 | pulumi stack rm --yes 15 | popd 16 | -------------------------------------------------------------------------------- /stack-examples/yaml/ext_s3_bucket_stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: pulumi-aws-secrets 13 | type: Opaque 14 | stringData: 15 | AWS_ACCESS_KEY_ID: "" 16 | AWS_SECRET_ACCESS_KEY: "" 17 | AWS_SESSION_TOKEN: "" 18 | 19 | --- 20 | apiVersion: pulumi.com/v1 21 | kind: Stack 22 | metadata: 23 | name: s3-bucket-stack-02 24 | spec: 25 | envRefs: 26 | PULUMI_ACCESS_TOKEN: 27 | type: Secret 28 | secret: 29 | name: pulumi-api-secret 30 | key: accessToken 31 | AWS_ACCESS_KEY_ID: 32 | type: Secret 33 | secret: 34 | name: pulumi-aws-secrets 35 | key: AWS_ACCESS_KEY_ID 36 | AWS_SECRET_ACCESS_KEY: 37 | type: Secret 38 | secret: 39 | name: pulumi-aws-secrets 40 | key: AWS_SECRET_ACCESS_KEY 41 | AWS_SESSION_TOKEN: 42 | type: Secret 43 | secret: 44 | name: pulumi-aws-secrets 45 | key: AWS_SESSION_TOKEN 46 | stack: joeduffy/s3-op-project/dev 47 | refresh: true 48 | destroyOnFinalize: true 49 | config: 50 | aws:region: us-east-2 51 | projectRepo: https://github.com/joeduffy/test-s3-op-project 52 | commit: cc5442870f1195216d6bc340c14f8ae7d28cf3e2 53 | -------------------------------------------------------------------------------- /stack-examples/yaml/git_auth_with_references.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: git-secret 13 | type: Opaque 14 | stringData: 15 | privateToken: 16 | data: 17 | # need to base64 SSH private key to set a valid YAML value 18 | sshPrivateKey: 19 | --- 20 | apiVersion: v1 21 | kind: Secret 22 | metadata: 23 | name: pulumi-aws-secrets 24 | type: Opaque 25 | stringData: 26 | AWS_ACCESS_KEY_ID: "" 27 | AWS_SECRET_ACCESS_KEY: "" 28 | --- 29 | apiVersion: pulumi.com/v1 30 | kind: Stack 31 | metadata: 32 | name: git-auth-test 33 | spec: 34 | envRefs: 35 | PULUMI_ACCESS_TOKEN: 36 | type: Secret 37 | secret: 38 | name: pulumi-api-secret 39 | key: accessToken 40 | AWS_ACCESS_KEY_ID: 41 | type: Secret 42 | secret: 43 | name: pulumi-aws-secrets 44 | key: AWS_ACCESS_KEY_ID 45 | AWS_SECRET_ACCESS_KEY: 46 | type: Secret 47 | secret: 48 | name: pulumi-aws-secrets 49 | key: AWS_SECRET_ACCESS_KEY 50 | gitAuth: 51 | # sshAuth: 52 | # sshPrivateKey: 53 | # type: Secret 54 | # secret: 55 | # name: git-secret 56 | # key: sshPrivateKey 57 | # password: 58 | # type: Secret 59 | # secret: 60 | # name: git-secret 61 | # key: password 62 | 63 | accessToken: 64 | type: Secret 65 | secret: 66 | name: git-secret 67 | key: privateToken 68 | stack: /git-auth-test/dev 69 | projectRepo: https://...private-repo.git 70 | commit: 71 | config: 72 | aws:region: us-west-2 73 | -------------------------------------------------------------------------------- /stack-examples/yaml/nginx_k8s_stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: pulumi.com/v1 10 | kind: Stack 11 | metadata: 12 | name: nginx-k8s-stack 13 | spec: 14 | envRefs: 15 | PULUMI_ACCESS_TOKEN: 16 | type: Secret 17 | secret: 18 | name: pulumi-api-secret 19 | key: accessToken 20 | stack: /k8s-nginx/dev 21 | projectRepo: https://github.com/pulumi/examples 22 | repoDir: /kubernetes-ts-nginx 23 | branch: "refs/heads/master" 24 | destroyOnFinalize: true 25 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3_bucket_stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: pulumi-aws-secrets 13 | type: Opaque 14 | stringData: 15 | AWS_ACCESS_KEY_ID: "" 16 | AWS_SECRET_ACCESS_KEY: "" 17 | AWS_SESSION_TOKEN: "" 18 | --- 19 | apiVersion: pulumi.com/v1 20 | kind: Stack 21 | metadata: 22 | name: s3-bucket-stack 23 | spec: 24 | envRefs: 25 | PULUMI_ACCESS_TOKEN: 26 | type: Secret 27 | secret: 28 | name: pulumi-api-secret 29 | key: accessToken 30 | AWS_ACCESS_KEY_ID: 31 | type: Secret 32 | secret: 33 | name: pulumi-aws-secrets 34 | key: AWS_ACCESS_KEY_ID 35 | AWS_SECRET_ACCESS_KEY: 36 | type: Secret 37 | secret: 38 | name: pulumi-aws-secrets 39 | key: AWS_SECRET_ACCESS_KEY 40 | AWS_SESSION_TOKEN: 41 | type: Secret 42 | secret: 43 | name: pulumi-aws-secrets 44 | key: AWS_SESSION_TOKEN 45 | stack: /s3-op-project/dev 46 | projectRepo: https://github.com/joeduffy/test-s3-op-project 47 | commit: 3edeafe930e2121358f56c7a9adc41f18505149e 48 | config: 49 | aws:region: us-east-2 50 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3_bucket_stack_access_token.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: git-secret 13 | type: Opaque 14 | stringData: 15 | accessToken: "" 16 | --- 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: pulumi-aws-secrets 21 | type: Opaque 22 | stringData: 23 | AWS_ACCESS_KEY_ID: "" 24 | AWS_SECRET_ACCESS_KEY: "" 25 | --- 26 | apiVersion: pulumi.com/v1 27 | kind: Stack 28 | metadata: 29 | name: s3-bucket-stack 30 | spec: 31 | envRefs: 32 | PULUMI_ACCESS_TOKEN: 33 | type: Secret 34 | secret: 35 | name: pulumi-api-secret 36 | key: accessToken 37 | AWS_ACCESS_KEY_ID: 38 | type: Secret 39 | secret: 40 | name: pulumi-aws-secrets 41 | key: AWS_ACCESS_KEY_ID 42 | AWS_SECRET_ACCESS_KEY: 43 | type: Secret 44 | secret: 45 | name: pulumi-aws-secrets 46 | key: AWS_SECRET_ACCESS_KEY 47 | gitAuthSecret: git-secret 48 | stack: joeduffy/s3-op-project/dev 49 | projectRepo: https://github.com/joeduffy/test-s3-op-project 50 | # Choose one of commit or branch. 51 | # commit: cc5442870f1195216d6bc340c14f8ae7d28cf3e2 52 | branch: refs/heads/master 53 | config: 54 | aws:region: us-east-2 55 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3_bucket_stack_basic_auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: git-secret 13 | type: Opaque 14 | stringData: 15 | username: "" 16 | password: "" 17 | --- 18 | apiVersion: v1 19 | kind: Secret 20 | metadata: 21 | name: pulumi-aws-secrets 22 | type: Opaque 23 | stringData: 24 | AWS_ACCESS_KEY_ID: "" 25 | AWS_SECRET_ACCESS_KEY: "" 26 | --- 27 | apiVersion: pulumi.com/v1 28 | kind: Stack 29 | metadata: 30 | name: s3-bucket-stack 31 | spec: 32 | envRefs: 33 | PULUMI_ACCESS_TOKEN: 34 | type: Secret 35 | secret: 36 | name: pulumi-api-secret 37 | key: accessToken 38 | AWS_ACCESS_KEY_ID: 39 | type: Secret 40 | secret: 41 | name: pulumi-aws-secrets 42 | key: AWS_ACCESS_KEY_ID 43 | AWS_SECRET_ACCESS_KEY: 44 | type: Secret 45 | secret: 46 | name: pulumi-aws-secrets 47 | key: AWS_SECRET_ACCESS_KEY 48 | gitAuthSecret: git-secret 49 | stack: joeduffy/s3-op-project/dev 50 | projectRepo: https://github.com/joeduffy/test-s3-op-project 51 | # Choose one of branch or commit 52 | # branch: refs/heads/master 53 | commit: cc5442870f1195216d6bc340c14f8ae7d28cf3e2 54 | config: 55 | aws:region: us-east-2 56 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3_bucket_stack_ssh.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-api-secret 5 | type: Opaque 6 | stringData: 7 | accessToken: "" 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: git-secret 13 | type: Opaque 14 | data: 15 | # need to base64 SSH private key to set a valid YAML value 16 | sshPrivateKey: "" 17 | password: "" 18 | --- 19 | apiVersion: v1 20 | kind: Secret 21 | metadata: 22 | name: pulumi-aws-secrets 23 | type: Opaque 24 | stringData: 25 | AWS_ACCESS_KEY_ID: "" 26 | AWS_SECRET_ACCESS_KEY: "" 27 | --- 28 | apiVersion: pulumi.com/v1 29 | kind: Stack 30 | metadata: 31 | name: s3-bucket-stack 32 | spec: 33 | envRefs: 34 | PULUMI_ACCESS_TOKEN: 35 | type: Secret 36 | secret: 37 | name: pulumi-api-secret 38 | key: accessToken 39 | AWS_ACCESS_KEY_ID: 40 | type: Secret 41 | secret: 42 | name: pulumi-aws-secrets 43 | key: AWS_ACCESS_KEY_ID 44 | AWS_SECRET_ACCESS_KEY: 45 | type: Secret 46 | secret: 47 | name: pulumi-aws-secrets 48 | key: AWS_SECRET_ACCESS_KEY 49 | gitAuthSecret: git-secret 50 | stack: joeduffy/s3-op-project/dev 51 | projectRepo: git@github.com:joeduffy/test-s3-op-project.git 52 | commit: cc5442870f1195216d6bc340c14f8ae7d28cf3e2 53 | config: 54 | aws:region: us-east-2 55 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3backend/nginx_k8s_stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: aws-creds-secret 5 | type: Opaque 6 | stringData: 7 | AWS_ACCESS_KEY_ID: "" 8 | AWS_SECRET_ACCESS_KEY: "" 9 | AWS_SESSION_TOKEN: "" 10 | --- 11 | apiVersion: pulumi.com/v1 12 | kind: Stack 13 | metadata: 14 | name: nginx-k8s-stack 15 | spec: 16 | backend: "s3://" 17 | secretsProvider: "awskms:///?region=" 18 | envRefs: 19 | AWS_DEFAULT_REGION: 20 | type: Literal 21 | literal: 22 | value: 23 | AWS_ACCESS_KEY_ID: 24 | type: Secret 25 | secret: 26 | name: aws-creds-secret 27 | key: AWS_ACCESS_KEY_ID 28 | AWS_SECRET_ACCESS_KEY: 29 | type: Secret 30 | secret: 31 | name: aws-creds-secret 32 | key: AWS_SECRET_ACCESS_KEY 33 | AWS_SESSION_TOKEN: 34 | type: Secret 35 | secret: 36 | name: aws-creds-secret 37 | key: AWS_SESSION_TOKEN 38 | stack: "s3backend.nginx.dev" 39 | projectRepo: https://github.com/pulumi/examples 40 | repoDir: /kubernetes-ts-nginx 41 | branch: "refs/heads/master" 42 | destroyOnFinalize: true 43 | -------------------------------------------------------------------------------- /stack-examples/yaml/s3backend/s3_bucket_stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: pulumi-aws-secrets 5 | type: Opaque 6 | stringData: 7 | AWS_ACCESS_KEY_ID: "" 8 | AWS_SECRET_ACCESS_KEY: "" 9 | AWS_SESSION_TOKEN: "" 10 | --- 11 | apiVersion: pulumi.com/v1 12 | kind: Stack 13 | metadata: 14 | name: s3-bucket-stack 15 | spec: 16 | backend: "s3://" 17 | secretsProvider: "awskms:///?region=" 18 | envRefs: 19 | AWS_DEFAULT_REGION: 20 | type: Literal 21 | literal: 22 | value: 23 | AWS_ACCESS_KEY_ID: 24 | type: Secret 25 | secret: 26 | name: pulumi-aws-secrets 27 | key: AWS_ACCESS_KEY_ID 28 | AWS_SECRET_ACCESS_KEY: 29 | type: Secret 30 | secret: 31 | name: pulumi-aws-secrets 32 | key: AWS_SECRET_ACCESS_KEY 33 | AWS_SESSION_TOKEN: 34 | type: Secret 35 | secret: 36 | name: pulumi-aws-secrets 37 | key: AWS_SESSION_TOKEN 38 | stack: "s3backend.s3-op-project.dev" 39 | projectRepo: https://github.com/joeduffy/test-s3-op-project 40 | commit: 3edeafe930e2121358f56c7a9adc41f18505149e 41 | config: 42 | aws:region: us-east-2 43 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2025, Pulumi Corporation. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tools 16 | // +build tools 17 | 18 | // Place any runtime dependencies as imports in this file. 19 | // Go modules will be forced to download and install them. 20 | package tools 21 | 22 | import _ "github.com/onsi/ginkgo/v2/ginkgo" 23 | --------------------------------------------------------------------------------