From a6638e66eecb3535ff348aab2f6ea6a2c5c30fe1 Mon Sep 17 00:00:00 2001 From: Danielle Lancashire Date: Thu, 1 Feb 2024 17:01:12 +0100 Subject: [PATCH] initial commit Signed-off-by: Danielle Lancashire --- .dockerignore | 3 + .envrc | 1 + .github/CODEOWNERS | 9 + .github/dependabot.yml | 13 + .github/workflows/build.yaml | 111 + .github/workflows/container.yaml | 77 + .github/workflows/docs.yaml | 24 + .github/workflows/sample-apps.yaml | 42 + .github/workflows/smoketest.yaml | 55 + .gitignore | 31 + .golangci.yaml | 102 + CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 92 + Dockerfile | 32 + LICENSE | 13 + Makefile | 231 ++ PROJECT | 37 + README.md | 26 + api/v1/groupversion_info.go | 36 + api/v1/spinapp_types.go | 229 ++ api/v1/spinappexecutor_types.go | 61 + api/v1/zz_generated.deepcopy.go | 406 ++++ apps/README.md | 3 + apps/cpu-load-gen/.gitignore | 2 + apps/cpu-load-gen/README.md | 18 + apps/cpu-load-gen/go.mod | 7 + apps/cpu-load-gen/go.sum | 4 + apps/cpu-load-gen/main.go | 25 + apps/cpu-load-gen/spin.toml | 18 + apps/hello-world/.gitignore | 2 + apps/hello-world/README.md | 3 + apps/hello-world/go.mod | 7 + apps/hello-world/go.sum | 4 + apps/hello-world/main.go | 17 + apps/hello-world/spin.toml | 18 + apps/order-processor/.gitignore | 2 + apps/order-processor/go.mod | 5 + apps/order-processor/go.sum | 2 + apps/order-processor/main.go | 21 + apps/order-processor/spin.toml | 20 + apps/variabletester/.gitignore | 2 + apps/variabletester/Dockerfile | 16 + apps/variabletester/go.mod | 7 + apps/variabletester/go.sum | 4 + apps/variabletester/main.go | 24 + apps/variabletester/spin.toml | 25 + charts/spin-operator/.helmignore | 23 + charts/spin-operator/Chart.lock | 9 + charts/spin-operator/Chart.yaml | 32 + charts/spin-operator/README.md | 89 + charts/spin-operator/templates/NOTES.txt | 39 + charts/spin-operator/templates/_helpers.tpl | 62 + .../spin-operator/templates/deployment.yaml | 82 + .../templates/leader-election-rbac.yaml | 59 + .../spin-operator/templates/manager-rbac.yaml | 101 + .../templates/metrics-reader-rbac.yaml | 14 + .../templates/metrics-service.yaml | 17 + .../mutating-webhook-configuration.yaml | 29 + .../spin-operator/templates/proxy-rbac.yaml | 40 + .../templates/selfsigned-issuer.yaml | 11 + .../templates/serviceaccount.yaml | 11 + .../spin-operator/templates/serving-cert.yaml | 19 + .../validating-webhook-configuration.yaml | 29 + .../templates/webhook-service.yaml | 16 + charts/spin-operator/values.yaml | 63 + cmd/main.go | 145 ++ config/certmanager/certificate.yaml | 39 + config/certmanager/kustomization.yaml | 5 + config/certmanager/kustomizeconfig.yaml | 8 + ...ore.spinoperator.dev_spinappexecutors.yaml | 44 + .../bases/core.spinoperator.dev_spinapps.yaml | 2162 +++++++++++++++++ .../crd/bases/spin.fermyon.com_spinapps.yaml | 310 +++ config/crd/kustomization.yaml | 26 + config/crd/kustomizeconfig.yaml | 19 + .../cainjection_in_spinappexecutors.yaml | 7 + .../crd/patches/cainjection_in_spinapps.yaml | 7 + .../patches/webhook_in_spinappexecutors.yaml | 16 + config/crd/patches/webhook_in_spinapps.yaml | 16 + config/default/kustomization.yaml | 142 ++ config/default/manager_auth_proxy_patch.yaml | 40 + config/default/manager_config_patch.yaml | 10 + config/default/manager_webhook_patch.yaml | 23 + config/default/webhookcainjection_patch.yaml | 29 + config/manager/kustomization.yaml | 2 + config/manager/manager.yaml | 103 + config/prometheus/kustomization.yaml | 2 + config/prometheus/monitor.yaml | 25 + .../rbac/auth_proxy_client_clusterrole.yaml | 16 + config/rbac/auth_proxy_role.yaml | 24 + config/rbac/auth_proxy_role_binding.yaml | 19 + config/rbac/auth_proxy_service.yaml | 21 + config/rbac/kustomization.yaml | 18 + config/rbac/leader_election_role.yaml | 44 + config/rbac/leader_election_role_binding.yaml | 19 + config/rbac/role.yaml | 82 + config/rbac/role_binding.yaml | 19 + config/rbac/service_account.yaml | 12 + config/rbac/spinapp_editor_role.yaml | 31 + config/rbac/spinapp_viewer_role.yaml | 27 + config/rbac/spinappexecutor_editor_role.yaml | 31 + config/rbac/spinappexecutor_viewer_role.yaml | 27 + config/samples/annotations.yaml | 15 + config/samples/cyclotron.yaml | 8 + config/samples/hpa.yaml | 35 + config/samples/kustomization.yaml | 15 + config/samples/private-image.yaml | 12 + config/samples/probes.yaml | 15 + config/samples/redis.yaml | 9 + config/samples/resources.yaml | 15 + config/samples/runtime-config.yaml | 18 + config/samples/shim-executor.yaml | 5 + config/samples/simple.yaml | 8 + config/samples/variables.yaml | 11 + config/samples/volume-mount.yaml | 42 + config/webhook/kustomization.yaml | 6 + config/webhook/kustomizeconfig.yaml | 22 + config/webhook/manifests.yaml | 92 + config/webhook/service.yaml | 19 + documentation/content/contributing.md | 7 + .../custom-resource-definition-reference.md | 119 + documentation/content/deploying-with-helm.md | 100 + documentation/content/glossary-of-terms.md | 103 + documentation/content/integrations.md | 23 + documentation/content/operator_development.md | 129 + documentation/content/prerequisites.md | 40 + documentation/content/project-goals.md | 8 + documentation/content/project-overview.md | 5 + documentation/content/quickstart.md | 41 + documentation/content/running-locally.md | 107 + documentation/content/running-on-a-cluster.md | 32 + .../scaling-spinapp-on-k8s-with-hpa.md | 199 ++ documentation/content/share-images.md | 14 + documentation/content/troubleshooting.md | 100 + documentation/content/uninstall.md | 36 + flake.lock | 124 + flake.nix | 40 + format.Dockerfile | 12 + go.mod | 71 + go.sum | 197 ++ hack/boilerplate.go.txt | 15 + hack/provision-minikube.sh | 39 + hack/runtime-config-to-secret.sh | 43 + internal/constants/constants.go | 22 + internal/controller/deployment.go | 152 ++ internal/controller/deployment_test.go | 243 ++ internal/controller/service.go | 59 + internal/controller/service_test.go | 31 + internal/controller/spinapp_controller.go | 401 +++ .../controller/spinapp_controller_test.go | 107 + .../controller/spinappexecutor_controller.go | 121 + .../spinappexecutor_controller_test.go | 90 + internal/logging/logr_logger.go | 85 + internal/webhook/admission.go | 22 + internal/webhook/admission_test.go | 260 ++ internal/webhook/spinapp_defaulting.go | 69 + internal/webhook/spinapp_defaulting_test.go | 26 + internal/webhook/spinapp_validating.go | 125 + internal/webhook/spinapp_validating_test.go | 63 + .../webhook/spinappexecutor_defaulting.go | 28 + .../spinappexecutor_defaulting_test.go | 3 + .../webhook/spinappexecutor_validating.go | 54 + .../spinappexecutor_validating_test.go | 3 + pkg/spinapp/spinapp.go | 37 + result | 1 + spin-runtime-class.yaml | 5 + 165 files changed, 10163 insertions(+) create mode 100644 .dockerignore create mode 100644 .envrc create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/container.yaml create mode 100644 .github/workflows/docs.yaml create mode 100644 .github/workflows/sample-apps.yaml create mode 100644 .github/workflows/smoketest.yaml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 PROJECT create mode 100644 README.md create mode 100644 api/v1/groupversion_info.go create mode 100644 api/v1/spinapp_types.go create mode 100644 api/v1/spinappexecutor_types.go create mode 100644 api/v1/zz_generated.deepcopy.go create mode 100644 apps/README.md create mode 100644 apps/cpu-load-gen/.gitignore create mode 100644 apps/cpu-load-gen/README.md create mode 100644 apps/cpu-load-gen/go.mod create mode 100644 apps/cpu-load-gen/go.sum create mode 100644 apps/cpu-load-gen/main.go create mode 100644 apps/cpu-load-gen/spin.toml create mode 100644 apps/hello-world/.gitignore create mode 100644 apps/hello-world/README.md create mode 100644 apps/hello-world/go.mod create mode 100644 apps/hello-world/go.sum create mode 100644 apps/hello-world/main.go create mode 100644 apps/hello-world/spin.toml create mode 100644 apps/order-processor/.gitignore create mode 100644 apps/order-processor/go.mod create mode 100644 apps/order-processor/go.sum create mode 100644 apps/order-processor/main.go create mode 100644 apps/order-processor/spin.toml create mode 100644 apps/variabletester/.gitignore create mode 100644 apps/variabletester/Dockerfile create mode 100644 apps/variabletester/go.mod create mode 100644 apps/variabletester/go.sum create mode 100644 apps/variabletester/main.go create mode 100644 apps/variabletester/spin.toml create mode 100644 charts/spin-operator/.helmignore create mode 100644 charts/spin-operator/Chart.lock create mode 100644 charts/spin-operator/Chart.yaml create mode 100644 charts/spin-operator/README.md create mode 100644 charts/spin-operator/templates/NOTES.txt create mode 100644 charts/spin-operator/templates/_helpers.tpl create mode 100644 charts/spin-operator/templates/deployment.yaml create mode 100644 charts/spin-operator/templates/leader-election-rbac.yaml create mode 100644 charts/spin-operator/templates/manager-rbac.yaml create mode 100644 charts/spin-operator/templates/metrics-reader-rbac.yaml create mode 100644 charts/spin-operator/templates/metrics-service.yaml create mode 100644 charts/spin-operator/templates/mutating-webhook-configuration.yaml create mode 100644 charts/spin-operator/templates/proxy-rbac.yaml create mode 100644 charts/spin-operator/templates/selfsigned-issuer.yaml create mode 100644 charts/spin-operator/templates/serviceaccount.yaml create mode 100644 charts/spin-operator/templates/serving-cert.yaml create mode 100644 charts/spin-operator/templates/validating-webhook-configuration.yaml create mode 100644 charts/spin-operator/templates/webhook-service.yaml create mode 100644 charts/spin-operator/values.yaml create mode 100644 cmd/main.go create mode 100644 config/certmanager/certificate.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/crd/bases/core.spinoperator.dev_spinappexecutors.yaml create mode 100644 config/crd/bases/core.spinoperator.dev_spinapps.yaml create mode 100644 config/crd/bases/spin.fermyon.com_spinapps.yaml create mode 100644 config/crd/kustomization.yaml create mode 100644 config/crd/kustomizeconfig.yaml create mode 100644 config/crd/patches/cainjection_in_spinappexecutors.yaml create mode 100644 config/crd/patches/cainjection_in_spinapps.yaml create mode 100644 config/crd/patches/webhook_in_spinappexecutors.yaml create mode 100644 config/crd/patches/webhook_in_spinapps.yaml create mode 100644 config/default/kustomization.yaml create mode 100644 config/default/manager_auth_proxy_patch.yaml create mode 100644 config/default/manager_config_patch.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/default/webhookcainjection_patch.yaml create mode 100644 config/manager/kustomization.yaml create mode 100644 config/manager/manager.yaml create mode 100644 config/prometheus/kustomization.yaml create mode 100644 config/prometheus/monitor.yaml create mode 100644 config/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 config/rbac/auth_proxy_role.yaml create mode 100644 config/rbac/auth_proxy_role_binding.yaml create mode 100644 config/rbac/auth_proxy_service.yaml create mode 100644 config/rbac/kustomization.yaml create mode 100644 config/rbac/leader_election_role.yaml create mode 100644 config/rbac/leader_election_role_binding.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/rbac/role_binding.yaml create mode 100644 config/rbac/service_account.yaml create mode 100644 config/rbac/spinapp_editor_role.yaml create mode 100644 config/rbac/spinapp_viewer_role.yaml create mode 100644 config/rbac/spinappexecutor_editor_role.yaml create mode 100644 config/rbac/spinappexecutor_viewer_role.yaml create mode 100644 config/samples/annotations.yaml create mode 100644 config/samples/cyclotron.yaml create mode 100644 config/samples/hpa.yaml create mode 100644 config/samples/kustomization.yaml create mode 100644 config/samples/private-image.yaml create mode 100644 config/samples/probes.yaml create mode 100644 config/samples/redis.yaml create mode 100644 config/samples/resources.yaml create mode 100644 config/samples/runtime-config.yaml create mode 100644 config/samples/shim-executor.yaml create mode 100644 config/samples/simple.yaml create mode 100644 config/samples/variables.yaml create mode 100644 config/samples/volume-mount.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml create mode 100644 documentation/content/contributing.md create mode 100644 documentation/content/custom-resource-definition-reference.md create mode 100644 documentation/content/deploying-with-helm.md create mode 100644 documentation/content/glossary-of-terms.md create mode 100644 documentation/content/integrations.md create mode 100644 documentation/content/operator_development.md create mode 100644 documentation/content/prerequisites.md create mode 100644 documentation/content/project-goals.md create mode 100644 documentation/content/project-overview.md create mode 100644 documentation/content/quickstart.md create mode 100644 documentation/content/running-locally.md create mode 100644 documentation/content/running-on-a-cluster.md create mode 100644 documentation/content/scaling-spinapp-on-k8s-with-hpa.md create mode 100644 documentation/content/share-images.md create mode 100644 documentation/content/troubleshooting.md create mode 100644 documentation/content/uninstall.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 format.Dockerfile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100755 hack/provision-minikube.sh create mode 100755 hack/runtime-config-to-secret.sh create mode 100644 internal/constants/constants.go create mode 100644 internal/controller/deployment.go create mode 100644 internal/controller/deployment_test.go create mode 100644 internal/controller/service.go create mode 100644 internal/controller/service_test.go create mode 100644 internal/controller/spinapp_controller.go create mode 100644 internal/controller/spinapp_controller_test.go create mode 100644 internal/controller/spinappexecutor_controller.go create mode 100644 internal/controller/spinappexecutor_controller_test.go create mode 100644 internal/logging/logr_logger.go create mode 100644 internal/webhook/admission.go create mode 100644 internal/webhook/admission_test.go create mode 100644 internal/webhook/spinapp_defaulting.go create mode 100644 internal/webhook/spinapp_defaulting_test.go create mode 100644 internal/webhook/spinapp_validating.go create mode 100644 internal/webhook/spinapp_validating_test.go create mode 100644 internal/webhook/spinappexecutor_defaulting.go create mode 100644 internal/webhook/spinappexecutor_defaulting_test.go create mode 100644 internal/webhook/spinappexecutor_validating.go create mode 100644 internal/webhook/spinappexecutor_validating_test.go create mode 100644 pkg/spinapp/spinapp.go create mode 120000 result create mode 100644 spin-runtime-class.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a3aab7af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..44610e56 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..db6e0004 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# CODEOWNERS +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# NOTE: Order is important; the last matching pattern takes the most precedence. When someone opens a pull request that +# only modifies files under a certain matching pattern, only those code owners will be requested for a review. + +# These owners will be the default owners for everything in the repository. Unless a later match takes precedence, they +# will be requested for review when someone opens a pull request. +* @calebschoepp @endocrimes @lann @michelleN diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..556c49ca --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..b890294c --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,111 @@ +name: Go Build and Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: write + packages: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + cache: true + + - name: Setup gotestsum + uses: autero1/action-gotestsum@v2.0.0 + with: + gotestsum_version: "1.8.2" + + - name: Install dependencies + run: go mod download + + - name: Build + run: CGO_ENABLED=0 go build -v ./... + + - name: Setup EnvTest + run: make envtest + + - name: Test + run: | + mkdir .results + gotestsum \ + --junitfile .results/results.xml \ + --jsonfile .results/results.json \ + --format testname \ + -- -coverprofile=.results/cover.out ./... + + - name: Test Summary + uses: test-summary/action@v2 + with: + paths: ".results/results.xml" + if: always() + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: results.xml + path: ./.results/results.xml + if: always() + - name: Upload test coverage + uses: actions/upload-artifact@v4 + with: + name: cover.out + path: ./.results/cover.out + if: always() + - name: Upload Go test results json + uses: actions/upload-artifact@v4 + with: + name: results.json + path: ./.results/results.json + + lint_go: + name: lint go + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + - uses: actions/checkout@v4 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.55.2 + args: --timeout=10m + + lint_shell: + name: lint shell + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + + lint_chart: + name: lint chart + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21.x' + cache: true + - name: Install dependencies + run: go mod download + - name: Install helm + uses: Azure/setup-helm@v3 + with: + version: v3.14.0 + - name: Lint chart + run: make helm-lint + diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml new file mode 100644 index 00000000..863ec31f --- /dev/null +++ b/.github/workflows/container.yaml @@ -0,0 +1,77 @@ +name: Docker + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup version info + run: echo "VERSION=$(date +%Y%m%d-%H%M%S)-g$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and Push PR - Ephemeral + uses: docker/build-push-action@v5 + if: github.event_name == 'pull_request' + with: + context: . + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ttl.sh/spoopy-operator-pr-${{ github.event.pull_request.number }}:24h + - uses: mshick/add-pr-comment@v2 + if: (github.event_name == 'pull_request') && ${{ success() }} + with: + message: | + This PR now has an image available for testing: + ``` + ttl.sh/spoopy-operator-pr-${{ github.event.pull_request.number }}:24h + ``` + + - name: Build and Push + uses: docker/build-push-action@v5 + if: github.event_name != 'pull_request' + with: + context: . + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ghcr.io/spinkube/spin-operator:${{ env.VERSION }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000..0660c686 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,24 @@ +name: Documentation + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint-markdown: + name: Lint all markdown files + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Lint markdown + run: | + make lint-markdown diff --git a/.github/workflows/sample-apps.yaml b/.github/workflows/sample-apps.yaml new file mode 100644 index 00000000..87b057d8 --- /dev/null +++ b/.github/workflows/sample-apps.yaml @@ -0,0 +1,42 @@ +name: Publish Sample Apps + +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + +jobs: + publish-image: + name: Publish sample app images + runs-on: ubuntu-latest + strategy: + matrix: + app: [cpu-load-gen, hello-world] + env: + IMAGE_NAME: ${{ github.repository }} + + steps: + - uses: actions/checkout@v4 + + - name: Set the release version + shell: bash + run: | + echo "RELEASE_VERSION=$(date +%Y%m%d-%H%M%S)-g$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Install Spin + uses: fermyon/actions/spin/setup@v1 + + - name: Install TinyGo + uses: acifani/setup-tinygo@v2 + + - name: Build and push versioned image + uses: fermyon/actions/spin/push@v1 + with: + registry: ${{ env.REGISTRY }} + registry_username: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + registry_reference: "ghcr.io/spinkube/spin-operator/${{ matrix.app }}:${{ env.RELEASE_VERSION }}" + manifest_file: apps/${{ matrix.app }}/spin.toml diff --git a/.github/workflows/smoketest.yaml b/.github/workflows/smoketest.yaml new file mode 100644 index 00000000..4fcb581d --- /dev/null +++ b/.github/workflows/smoketest.yaml @@ -0,0 +1,55 @@ +name: Smoketest + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.21.x" + + - name: setup k3d + uses: engineerd/configurator@v0.0.10 + with: + name: k3d + url: https://github.com/k3d-io/k3d/releases/download/v5.6.0/k3d-linux-amd64 + + - name: start k3d cluster + run: | + k3d cluster create wasm-cluster \ + --image ghcr.io/deislabs/containerd-wasm-shims/examples/k3d:v0.10.0 \ + --port "8081:80@loadbalancer" \ + --agents 2 + + - name: apply runtime class + run: kubectl apply -f spin-runtime-class.yaml + + - name: start controller + timeout-minutes: 5 + run: | + make install + make run & + + timeout 300s bash -c 'until curl -s http://localhost:8082/healthz; do echo "waiting for controller to start"; sleep 2; done' + echo "" + echo "controller started successfully" + + - name: run spin app + run: | + kubectl apply -f config/samples/shim-executor.yaml + kubectl apply -f config/samples/simple.yaml + kubectl rollout status deployment simple-spinapp --timeout 90s + + kubectl port-forward svc/simple-spinapp 8083:80 & + timeout 15s bash -c 'until curl -f -vvv http://localhost:8083/hello; do sleep 2; done' + + - name: Verify curl + run: curl localhost:8083/hello diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..062533bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ +.direnv/ + +# Helm chart dependencies + +charts/spin-operator/charts diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..3c258434 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,102 @@ +issues: + # The default exclude list seems rather aggressive, opt-in when needed instead + exclude-use-default: false + + exclude-rules: + # Duplicated errcheck checks + - linters: [gosec] + text: G104 + # Duplicated errcheck checks + - linters: [staticcheck] + text: SA5001 + # We don't require comments on everything + - linters: [golint] + text: should have( a package)? comment + # very long lines are ok if they're URLs + - linters: [lll] + source: https?:// + # very long lines are ok if they're go:generate + - linters: [lll] + source: "^//go:generate " + # Ignore errcheck on deferred Close + - linters: [errcheck] + source: ^\s*defer .*\.Close(.*)$ + # Ignore ineffective assignments to ctx + - linters: [ineffassign] + source: ^\s*ctx.*=.*$ + - linters: [staticcheck] + source: ^\s*ctx.*=.*$ + # Don't require package docs + - linters: [stylecheck] + text: ST1000 + # Unparam is allowed in tests + - linters: [unparam] + path: _test\.go + +linters: + disable-all: true + enable: + - bodyclose + - depguard + - errcheck + - errorlint + - goconst + - gocyclo + - gofmt + - goimports + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - forbidigo + +linters-settings: + govet: + check-shadowing: false + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + dupl: + # Don't detect small duplications, but if we're duplicating functions across + # packages, we should consider refactoring. + threshold: 100 + depguard: + rules: + main: + files: + - '$all' + deny: + - pkg: "github.com/pkg/errors" + desc: "use Go 1.13 errors instead: https://blog.golang.org/go1.13-errors" + testing: + files: ['$test'] + deny: + - pkg: "github.com/stretchr/testify/assert" + desc: "use github.com/stretchr/testify/require instead" + goconst: + min-len: 8 + min-occurrences: 10 + lll: + line-length: 180 + forbidigo: + # Forbid the following identifiers (list of regexp). + # Default: ["^(fmt\\.Print(|f|ln)|print|println)$"] + forbid: + # Builtin function: + - ^print.*$ + - p: ^fmt\.Print.*$ + msg: Do not commit print statements. + - p: ^os\.Getenv + msg: Pull values through configuration rather than os.Getenv diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d0a69378 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +This project subscribes to the Fermyon [Code of Conduct](https://www.fermyon.com/code-of-conduct). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c74d96b2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# CONTRIBUTING + +We are delighted that you are interested in making spin-operator better! Thank you! This document will guide you through +making your first contribution to the project. We welcome and appreciate contributions of all types - opening issues, +fixing typos, adding examples, one-liner code fixes, tests, or complete features. + +First, any contribution and interaction on any Fermyon project MUST follow our [Code of +Conduct](https://www.fermyon.com/code-of-conduct). Thank you for being part of an inclusive and open community! + +If you plan on contributing anything complex, please go through the [open +issues](https://github.com/spinkube/spin-operator/issues) and [PR queue](https://github.com/spinkube/spin-operator/pulls) +first to make sure someone else has not started working on it. If it doesn't exist already, please [open an +issue](https://github.com/spinkube/spin-operator/issues/new) so you have a chance to get feedback from the community and +the maintainers before you start working on your feature. + +## Making Code Contributions to spin-operator + +The following guide is intended to make sure your contribution can get merged as soon as possible. First, make sure you +have the following prerequisites configured: + +- `go` version v1.20.0+ +- `docker` version 17.03+. +- `kubectl` version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster (This project is being developed using [`k3d`](https://k3d.io/v5.6.0/)) +- `make` +- please ensure you [configure adding a GPG signature to your + commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) + as well as appending a sign-off message (`git commit -S -s`) + +Once you have set up the prerequisites and identified the contribution you want to make to spin-operator, make sure you +can correctly build the project: + +```console +# clone the repository +$ git clone https://github.com/spinkube/spin-operator && cd spin-operator +# add a new remote pointing to your fork of the project +$ git remote add fork https://github.com//spin-operator +# create a new branch for your work +$ git checkout -b + +# build spin-operator +$ make + +# make sure compilation is successful +$ ./bin/manager --help + +# run the tests and make sure they pass +$ make test +``` + +Now you should be ready to start making your contribution. To familiarize yourself with the spin-operator project, +please read the [README](https://github.com/spinkube/spin-operator). Since most of spin-operator is written in Go, we try +to follow the common Go coding conventions. If applicable, add unit or integration tests to ensure your contribution is +correct. + +## Before You Commit + +- Format the code (`go fmt ./...`) +- Run Clippy (`go vet ./...`) +- Run the lint task (`make lint` or `make lint-fix`) +- Build the project and run the tests (`make test`) + +spin-operator enforces lints and tests as part of continuous integration - running them locally will save you a +round-trip to your pull request! + +If everything works locally, you're ready to commit your changes. + +## Committing and Pushing Your Changes + +We require commits to be signed both with an email address and with a GPG signature. + +> Because of the way GitHub runs enforcement, the GPG signature isn't checked until after all tests have run. Be sure to +> GPG sign up front, as it can be a bit frustrating to wait for all the tests and then get blocked on the signature! + +```console +$ git commit -S -s -m "" +``` + +Some contributors like to follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) convention +for commit messages. + +We try to only keep useful changes as separate commits - if you prefer to commit often, please cleanup the commit +history before opening a pull request. + +Once you are happy with your changes you can push the branch to your fork: + +```console +# "fork" is the name of the git remote pointing to your fork +$ git push fork +``` + +Now you are ready to create a pull request. Thank you for your contribution! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..77448133 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 + +# Build the manager binary +FROM --platform=${BUILDPLATFORM} golang:1.21 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.sum ./ + +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY . . + +# Don't set a fallback value for TARGETARCH so that it defaults to the GOARCH default +# equivalent to `BUILDPLATFORM` - this ensures that `docker build .` will build for +# the users local arch. +RUN --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make golangci-build + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM --platform=${TARGETPLATFORM} gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/bin/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0d48de2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) Fermyon Technologies, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..754becea --- /dev/null +++ b/Makefile @@ -0,0 +1,231 @@ +COMMIT := $(shell git rev-parse HEAD) +COMMIT_SHORT := $(shell git rev-parse --short HEAD) +DATE := $(shell date +%Y-%m-%d) +BRANCH := $(shell git rev-parse --abbrev-ref HEAD) +VERSION ?= ${BRANCH}-${COMMIT_SHORT} +PKG_LDFLAGS := github.com/prometheus/common/version +LDFLAGS := -s -w -X ${PKG_LDFLAGS}.Version=${VERSION} -X ${PKG_LDFLAGS}.Revision=${COMMIT} -X ${PKG_LDFLAGS}.BuildDate=${DATE} -X ${PKG_LDFLAGS}.Branch=${BRANCH} + +# Image URL to use all building/pushing image targets +DEFAULT_IMG_REPO := ghcr.io/spinkube/spin-operator +IMG ?= $(DEFAULT_IMG_REPO):$(shell git rev-parse --short HEAD)-dev + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.28.3 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# we currently depend on Docker and `buildx` to ensure that we can build cross-arch +# images effectively. We may decide to change that in the future. +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./api/..." paths="./cmd/..." paths="./internal/..." paths="./pkg/..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." paths="./cmd/..." paths="./internal/..." paths="./pkg/..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + +GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.54.2 +golangci-lint: + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ + } + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +.PHONY: helm-lint +helm-lint: helm-generate ## Lint the Helm chart + $(HELM) lint ./charts/$(HELM_CHART) + +.PHONY: lint-markdown +lint-markdown: ## Lint markdown files + $(CONTAINER_TOOL) build --load -f format.Dockerfile -t markdown-formatter . + $(CONTAINER_TOOL) run -e PRETTIER_MODE=check -v .:/usr/spin-operator markdown-formatter + +.PHONY: lint-markdown-fix +lint-markdown-fix: ## Lint markdown files and perform fixes + $(CONTAINER_TOOL) build --load -f format.Dockerfile -t markdown-formatter . + $(CONTAINER_TOOL) run -e PRETTIER_MODE=write -v .:/usr/spin-operator markdown-formatter + +##@ Build + +.PHONY: golangci-build +golangci-build: ## Build manager binary. + go build -ldflags "${LDFLAGS}" -a -o bin/manager cmd/main.go + +.PHONY: build +build: manifests generate fmt vet golangci-build ## Build manager binary. + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run -ldflags "${LDFLAGS}" ./cmd/main.go + +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build --load -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +PLATFORMS ?= linux/arm64,linux/amd64 +.PHONY: docker-build-and-publish-all +docker-build-and-publish-all: ## Build the docker image for all supported platforms + $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} . + +##@ Package + +HELM_CHART := spin-operator + +.PHONY: helm-generate +helm-generate: manifests kustomize helmify ## Create/update the Helm chart based on kustomize manifests. (Note: CRDs not included) + $(KUSTOMIZE) build config/default | $(HELMIFY) -crd-dir -cert-manager-as-subchart -cert-manager-version v1.13.3 charts/$(HELM_CHART) + rm -rf charts/$(HELM_CHART)/crds + $(HELM) dep up charts/$(HELM_CHART) + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image $(DEFAULT_IMG_REPO)=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +HELM_RELEASE ?= $(HELM_CHART) +HELM_NAMESPACE ?= $(HELM_CHART) +IMG_REPO := $(shell echo "${IMG}" | cut -d ':' -f 1) +IMG_TAG := $(shell echo "${IMG}" | cut -d ':' -f 2) + +.PHONY: helm-install +helm-install: helm-generate ## Install the Helm chart onto the K8s cluster specified in ~/.kube/config. + $(HELM) upgrade --install \ + -n $(HELM_NAMESPACE) \ + --create-namespace \ + --set controllerManager.manager.image.repository=$(IMG_REPO) \ + --set controllerManager.manager.image.tag=$(IMG_TAG) \ + $(HELM_RELEASE) charts/$(HELM_CHART) + +.PHONY: helm-upgrade +helm-upgrade: helm-install ## Upgrade the Helm release. + +.PHONY: helm-uninstall +helm-uninstall: ## Delete the Helm release. + $(HELM) delete \ + -n $(HELM_NAMESPACE) \ + $(HELM_RELEASE) + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +HELM ?= helm +HELMIFY ?= $(LOCALBIN)/helmify + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.2.1 +CONTROLLER_TOOLS_VERSION ?= v0.13.0 +HELMIFY_VESRION ?= v0.4.10 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) + +.PHONY: helmify +helmify: $(HELMIFY) ## Download helmify locally if necessary. +$(HELMIFY): $(LOCALBIN) + test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@$(HELMIFY_VESRION) diff --git a/PROJECT b/PROJECT new file mode 100644 index 00000000..fa1ee155 --- /dev/null +++ b/PROJECT @@ -0,0 +1,37 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: spinoperator.dev +layout: +- go.kubebuilder.io/v4 +projectName: spin-operator +repo: github.com/spinkube/spin-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: spinoperator.dev + group: core + kind: SpinApp + path: github.com/spinkube/spin-operator/api/v1 + version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: spinoperator.dev + group: core + kind: SpinAppExecutor + path: github.com/spinkube/spin-operator/api/v1 + version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +version: "3" diff --git a/README.md b/README.md new file mode 100644 index 00000000..860b445a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +- [Spin Operator](#spin-operator) + - [Getting Started](#getting-started) + - [Contributing](#contributing) + - [Official Documentation](#official-documentation) + +# Spin Operator + +The spin operator enables deploying Spin applications to Kubernetes. It watches [SpinApp Custom Resources](./documentation/content/custom-resource-definition-reference.md) and realizes desired state in the Kubernetes cluster. This project was built using the Kubebuilder framework and contains a Spin App CRD and controller. + +## Getting Started + +See [Quickstart](./documentation/content/quickstart.md). + +## Contributing + +Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for a guide on how to contribute to this project. + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## Official Documentation + +The documentation is published at [https://TODO-use-hugo-and-github-pages](https://TODO-use-hugo-and-github-pages) + +Documentation source files are located in the [documentation](./documentation/) section. diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go new file mode 100644 index 00000000..f29a1042 --- /dev/null +++ b/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the spin v1 API group +// +kubebuilder:object:generate=true +// +groupName=core.spinoperator.dev +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "core.spinoperator.dev", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1/spinapp_types.go b/api/v1/spinapp_types.go new file mode 100644 index 00000000..93287856 --- /dev/null +++ b/api/v1/spinapp_types.go @@ -0,0 +1,229 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SpinAppSpec defines the desired state of SpinApp +// +// +kubebuilder:validation:Optional +type SpinAppSpec struct { + // Executor controls how this app is executed in the cluster. + // + // Defaults to whatever executor is available on the cluster. If multiple + // executors are available then the first executor in alphabetical order + // will be chosen. If no executors are available then no default will be set. + Executor string `json:"executor"` + + // Image is the source for this app. + // + // +kubebuilder:validation:Required + Image string `json:"image"` + + // ImagePullSecrets is a list of references to secrets in the same namespace to use for pulling the image. + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + + // Checks defines health checks that should be used by Kubernetes to monitor the application. + Checks HealthChecks `json:"checks,omitempty"` + + // Number of replicas to run. + Replicas int32 `json:"replicas"` + + // EnableAutoscaling indicates whether the app is allowed to autoscale. If + // true then the operator leaves the replica count of the underlying + // deployment to be managed by an external autoscaler (HPA/KEDA). Replicas + // cannot be defined if this is enabled. By default EnableAutoscaling is false. + // + // +kubebuilder:default:=false + EnableAutoscaling bool `json:"enableAutoscaling,omitempty"` + + // RuntimeConfig defines configuration to be applied at runtime for this app. + RuntimeConfig RuntimeConfig `json:"runtimeConfig,omitempty"` + + // Volumes defines the volumes to be mounted in the underlying pods. + Volumes []corev1.Volume `json:"volumes,omitempty"` + + // VolumeMounts defines how volumes are mounted in the underlying containers. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + + // Variables provide Kubernetes Bindings to Spin App Variables. + Variables []SpinVar `json:"variables,omitempty"` + + // ServiceAnnotations defines annotations to be applied to the underlying service. + ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` + + // DeploymentAnnotations defines annotations to be applied to the underlying deployment. + DeploymentAnnotations map[string]string `json:"deploymentAnnotations,omitempty"` + + // PodAnnotations defines annotations to be applied to the underlying pods. + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + + // Resources defines the resource requirements for this app. + Resources Resources `json:"resources,omitempty"` +} + +// SpinAppStatus defines the observed state of SpinApp +type SpinAppStatus struct { + // Represents the observations of a SpinApps's current state. + // SpinApp.status.conditions.type are: "Available" and "Progressing" + // SpinApp.status.conditions.status are one of True, False, Unknown. + // SpinApp.status.conditions.reason the value should be a CamelCase string and producers of specific + // condition types may define expected values and meanings for this field, and whether the values + // are considered a guaranteed API. + // SpinApp.status.conditions.Message is a human readable message indicating details about the transition. + // For further information see: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // ActiveScheduler is the name of the scheduler that is currently scheduling this SpinApp. + ActiveScheduler string `json:"activeScheduler,omitempty"` + + // Represents the current number of active replicas on the application deployment. + ReadyReplicas int32 `json:"readyReplicas,omitempty"` +} + +// SpinApp is the Schema for the spinapps API +// +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=".status.readyReplicas",name=Ready Replicas,type=integer +// +kubebuilder:printcolumn:JSONPath=".spec.executor",name=Executor,type=string +type SpinApp struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SpinAppSpec `json:"spec,omitempty"` + Status SpinAppStatus `json:"status,omitempty"` +} + +// SpinAppList contains a list of SpinApp +// +// +kubebuilder:object:root=true +type SpinAppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []SpinApp `json:"items"` +} + +// RuntimeConfig defines configuration to be applied at runtime for this app. +type RuntimeConfig struct { + // LoadFromSecret is the name of the secret to load runtime config from. The + // secret should have a single key named "runtime-config.toml" that contains + // the base64 encoded runtime config. + LoadFromSecret string `json:"loadFromSecret,omitempty"` +} + +// SpinVar defines a binding between a spin variable and a static or dynamic value. +type SpinVar struct { + // Name of the variable to bind. + Name string `json:"name"` + + // Value is the static value to bind to the variable. + // + // +optional + Value string `json:"value,omitempty"` + + // ValueFrom is a reference to dynamically bind the variable to. + // + // +optional + ValueFrom *corev1.EnvVarSource `json:"valueFrom,omitempty"` +} + +// Resources defines the resource requirements for this app. +type Resources struct { + // Limits describes the maximum amount of compute resources allowed. + Limits corev1.ResourceList `json:"limits,omitempty"` + + // Requests describes the minimum amount of compute resources required. + // If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + // otherwise to an implementation-defined value. Requests cannot exceed Limits. + Requests corev1.ResourceList `json:"requests,omitempty"` +} + +// HealthChecks defines configuration for readiness and liveness probes for the +// application. +type HealthChecks struct { + // Readiness defines the readiness probe for the application. + Readiness *HealthProbe `json:"readiness,omitempty"` + + // Liveness defines the liveness probe for the application. + Liveness *HealthProbe `json:"liveness,omitempty"` +} + +// HealthProbe defines an individual health check for an application. +type HealthProbe struct { + // HTTPGet describes a health check that should be performed using a GET request. + HTTPGet *HTTPHealthProbe `json:"httpGet,omitempty"` + + // Number of seconds after the app has started before liveness probes are initiated. + // Default 10s. + // + // +kubebuilder:default:=10 + InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"` + + // Number of seconds after which the probe times out. + // Defaults to 1 second. Minimum value is 1. + // + // +kubebuilder:default:=1 + TimeoutSeconds int32 `json:"timeoutSeconds,omitempty"` + + // How often (in seconds) to perform the probe. + // Default to 10 seconds. Minimum value is 1. + // + // +optional + // +kubebuilder:default:=10 + PeriodSeconds int32 `json:"periodSeconds,omitempty"` + + // Minimum consecutive successes for the probe to be considered successful after having failed. + // Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + // + // +optional + // +kubebuilder:default:=1 + SuccessThreshold int32 `json:"successThreshold,omitempty"` + + // Minimum consecutive failures for the probe to be considered failed after having succeeded. + // Defaults to 3. Minimum value is 1. + // + // +optional + // +kubebuilder:default:=3 + FailureThreshold int32 `json:"failureThreshold,omitempty"` +} + +// HTTPHealthProbe defines a HealthProbe that should use HTTP to call the application. +type HTTPHealthProbe struct { + // Path is the path that should be used when calling the application for a + // health check, e.g /healthz. + Path string `json:"path"` + + // HTTPHeaders are headers that should be included in the health check request. + // + // +optional + HTTPHeaders []HTTPHealthProbeHeader `json:"httpHeaders"` +} + +// HTTPHealthProbeHeader is an abstraction around a http header key/value pair. +type HTTPHealthProbeHeader struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func init() { + SchemeBuilder.Register(&SpinApp{}, &SpinAppList{}) +} diff --git a/api/v1/spinappexecutor_types.go b/api/v1/spinappexecutor_types.go new file mode 100644 index 00000000..301eb238 --- /dev/null +++ b/api/v1/spinappexecutor_types.go @@ -0,0 +1,61 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// SpinAppExecutorSpec defines the desired state of SpinAppExecutor +type SpinAppExecutorSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// SpinAppExecutorStatus defines the observed state of SpinAppExecutor +type SpinAppExecutorStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// SpinAppExecutor is the Schema for the spinappexecutors API +type SpinAppExecutor struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SpinAppExecutorSpec `json:"spec,omitempty"` + Status SpinAppExecutorStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SpinAppExecutorList contains a list of SpinAppExecutor +type SpinAppExecutorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SpinAppExecutor `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SpinAppExecutor{}, &SpinAppExecutorList{}) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..48bcb3e4 --- /dev/null +++ b/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,406 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPHealthProbe) DeepCopyInto(out *HTTPHealthProbe) { + *out = *in + if in.HTTPHeaders != nil { + in, out := &in.HTTPHeaders, &out.HTTPHeaders + *out = make([]HTTPHealthProbeHeader, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHealthProbe. +func (in *HTTPHealthProbe) DeepCopy() *HTTPHealthProbe { + if in == nil { + return nil + } + out := new(HTTPHealthProbe) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPHealthProbeHeader) DeepCopyInto(out *HTTPHealthProbeHeader) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHealthProbeHeader. +func (in *HTTPHealthProbeHeader) DeepCopy() *HTTPHealthProbeHeader { + if in == nil { + return nil + } + out := new(HTTPHealthProbeHeader) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthChecks) DeepCopyInto(out *HealthChecks) { + *out = *in + if in.Readiness != nil { + in, out := &in.Readiness, &out.Readiness + *out = new(HealthProbe) + (*in).DeepCopyInto(*out) + } + if in.Liveness != nil { + in, out := &in.Liveness, &out.Liveness + *out = new(HealthProbe) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthChecks. +func (in *HealthChecks) DeepCopy() *HealthChecks { + if in == nil { + return nil + } + out := new(HealthChecks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthProbe) DeepCopyInto(out *HealthProbe) { + *out = *in + if in.HTTPGet != nil { + in, out := &in.HTTPGet, &out.HTTPGet + *out = new(HTTPHealthProbe) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthProbe. +func (in *HealthProbe) DeepCopy() *HealthProbe { + if in == nil { + return nil + } + out := new(HealthProbe) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resources) DeepCopyInto(out *Resources) { + *out = *in + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = make(corev1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + if in.Requests != nil { + in, out := &in.Requests, &out.Requests + *out = make(corev1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. +func (in *Resources) DeepCopy() *Resources { + if in == nil { + return nil + } + out := new(Resources) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RuntimeConfig) DeepCopyInto(out *RuntimeConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeConfig. +func (in *RuntimeConfig) DeepCopy() *RuntimeConfig { + if in == nil { + return nil + } + out := new(RuntimeConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinApp) DeepCopyInto(out *SpinApp) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinApp. +func (in *SpinApp) DeepCopy() *SpinApp { + if in == nil { + return nil + } + out := new(SpinApp) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SpinApp) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppExecutor) DeepCopyInto(out *SpinAppExecutor) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppExecutor. +func (in *SpinAppExecutor) DeepCopy() *SpinAppExecutor { + if in == nil { + return nil + } + out := new(SpinAppExecutor) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SpinAppExecutor) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppExecutorList) DeepCopyInto(out *SpinAppExecutorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SpinAppExecutor, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppExecutorList. +func (in *SpinAppExecutorList) DeepCopy() *SpinAppExecutorList { + if in == nil { + return nil + } + out := new(SpinAppExecutorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SpinAppExecutorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppExecutorSpec) DeepCopyInto(out *SpinAppExecutorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppExecutorSpec. +func (in *SpinAppExecutorSpec) DeepCopy() *SpinAppExecutorSpec { + if in == nil { + return nil + } + out := new(SpinAppExecutorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppExecutorStatus) DeepCopyInto(out *SpinAppExecutorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppExecutorStatus. +func (in *SpinAppExecutorStatus) DeepCopy() *SpinAppExecutorStatus { + if in == nil { + return nil + } + out := new(SpinAppExecutorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppList) DeepCopyInto(out *SpinAppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SpinApp, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppList. +func (in *SpinAppList) DeepCopy() *SpinAppList { + if in == nil { + return nil + } + out := new(SpinAppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SpinAppList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppSpec) DeepCopyInto(out *SpinAppSpec) { + *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + in.Checks.DeepCopyInto(&out.Checks) + out.RuntimeConfig = in.RuntimeConfig + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]SpinVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ServiceAnnotations != nil { + in, out := &in.ServiceAnnotations, &out.ServiceAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.DeploymentAnnotations != nil { + in, out := &in.DeploymentAnnotations, &out.DeploymentAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppSpec. +func (in *SpinAppSpec) DeepCopy() *SpinAppSpec { + if in == nil { + return nil + } + out := new(SpinAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinAppStatus) DeepCopyInto(out *SpinAppStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinAppStatus. +func (in *SpinAppStatus) DeepCopy() *SpinAppStatus { + if in == nil { + return nil + } + out := new(SpinAppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpinVar) DeepCopyInto(out *SpinVar) { + *out = *in + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(corev1.EnvVarSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpinVar. +func (in *SpinVar) DeepCopy() *SpinVar { + if in == nil { + return nil + } + out := new(SpinVar) + in.DeepCopyInto(out) + return out +} diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 00000000..36c0e989 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,3 @@ +# Apps + +This directory contains source code for Spin apps that are used by the `spin-operator` samples found in `config/samples/` directory. When you add a new app you need to add it to the `publish-image` matrix in [`.github/workflows/sample-apps.yaml`](../.github/workflows/sample-apps.yaml) file. diff --git a/apps/cpu-load-gen/.gitignore b/apps/cpu-load-gen/.gitignore new file mode 100644 index 00000000..b5650104 --- /dev/null +++ b/apps/cpu-load-gen/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/apps/cpu-load-gen/README.md b/apps/cpu-load-gen/README.md new file mode 100644 index 00000000..6ac81f8c --- /dev/null +++ b/apps/cpu-load-gen/README.md @@ -0,0 +1,18 @@ +# cpu-load-gen + +A simple Spin application that generates CPU load by calculating the fibonacci sequence. + +## Development + +```bash +# Build it +spin build + +# Run it +spin up + +# Push it to registry to be used by SpinApp in spin-operator +spin registry push ttl.sh/cpu-load-gen:1h +``` + +TODO: Spin build and publish this image diff --git a/apps/cpu-load-gen/go.mod b/apps/cpu-load-gen/go.mod new file mode 100644 index 00000000..9d5894b6 --- /dev/null +++ b/apps/cpu-load-gen/go.mod @@ -0,0 +1,7 @@ +module github.com/cpu_load_gen + +go 1.20 + +require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da + +require github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/apps/cpu-load-gen/go.sum b/apps/cpu-load-gen/go.sum new file mode 100644 index 00000000..61f15c7a --- /dev/null +++ b/apps/cpu-load-gen/go.sum @@ -0,0 +1,4 @@ +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da h1:+VKaIRRCsRuKggpt7xDF5Euc0eSShnwLVFNC2evv2Qo= +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/apps/cpu-load-gen/main.go b/apps/cpu-load-gen/main.go new file mode 100644 index 00000000..d54c0514 --- /dev/null +++ b/apps/cpu-load-gen/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + x := 43 // Experimentally generates a reasonable amount of CPU load + fmt.Printf("Calculating fib(%d)\n", x) + fmt.Fprintf(w, "fib(%d) = %d\n", x, fib(x)) + }) +} + +func fib(n int) int { + if n < 2 { + return n + } + return fib(n-2) + fib(n-1) +} + +func main() {} diff --git a/apps/cpu-load-gen/spin.toml b/apps/cpu-load-gen/spin.toml new file mode 100644 index 00000000..5b7e9d29 --- /dev/null +++ b/apps/cpu-load-gen/spin.toml @@ -0,0 +1,18 @@ +spin_manifest_version = 2 + +[application] +name = "cpu-load-gen" +version = "0.1.0" +authors = ["Caleb Schoepp "] +description = "A simple Spin app that will generate lots of load on the CPU by computing large fibonacci sequences" + +[[trigger.http]] +route = "/..." +component = "cpu-load-gen" + +[component.cpu-load-gen] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.cpu-load-gen.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +watch = ["**/*.go", "go.mod"] diff --git a/apps/hello-world/.gitignore b/apps/hello-world/.gitignore new file mode 100644 index 00000000..b5650104 --- /dev/null +++ b/apps/hello-world/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/apps/hello-world/README.md b/apps/hello-world/README.md new file mode 100644 index 00000000..be72cded --- /dev/null +++ b/apps/hello-world/README.md @@ -0,0 +1,3 @@ +# hello-world + +This is a simple hello world Spin app to demonstrate running Spin in Kubernetes. diff --git a/apps/hello-world/go.mod b/apps/hello-world/go.mod new file mode 100644 index 00000000..ecfb0fc8 --- /dev/null +++ b/apps/hello-world/go.mod @@ -0,0 +1,7 @@ +module github.com/hello_world + +go 1.20 + +require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3 + +require github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/apps/hello-world/go.sum b/apps/hello-world/go.sum new file mode 100644 index 00000000..3246e36c --- /dev/null +++ b/apps/hello-world/go.sum @@ -0,0 +1,4 @@ +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3 h1:B69YRVsqBEdP23rPUyjFmz2BK73E42TCjqCa0d1RZ6M= +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/apps/hello-world/main.go b/apps/hello-world/main.go new file mode 100644 index 00000000..93d63a89 --- /dev/null +++ b/apps/hello-world/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintln(w, "Hello Fermyon!") + }) +} + +func main() {} diff --git a/apps/hello-world/spin.toml b/apps/hello-world/spin.toml new file mode 100644 index 00000000..b55488a8 --- /dev/null +++ b/apps/hello-world/spin.toml @@ -0,0 +1,18 @@ +spin_manifest_version = 2 + +[application] +name = "hello-world" +version = "0.1.0" +authors = ["Caleb Schoepp "] +description = "A simple hello world Spin app" + +[[trigger.http]] +route = "/..." +component = "hello-world" + +[component.hello-world] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.hello-world.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +watch = ["**/*.go", "go.mod"] diff --git a/apps/order-processor/.gitignore b/apps/order-processor/.gitignore new file mode 100644 index 00000000..b5650104 --- /dev/null +++ b/apps/order-processor/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/apps/order-processor/go.mod b/apps/order-processor/go.mod new file mode 100644 index 00000000..18f2e67c --- /dev/null +++ b/apps/order-processor/go.mod @@ -0,0 +1,5 @@ +module github.com/order_processor + +go 1.20 + +require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e diff --git a/apps/order-processor/go.sum b/apps/order-processor/go.sum new file mode 100644 index 00000000..4ab43ea4 --- /dev/null +++ b/apps/order-processor/go.sum @@ -0,0 +1,2 @@ +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e h1:489XksQPuLJG5uBsJ8nBSOaoUYQI+s2/RM/lJV68+SQ= +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= diff --git a/apps/order-processor/main.go b/apps/order-processor/main.go new file mode 100644 index 00000000..d89f12fe --- /dev/null +++ b/apps/order-processor/main.go @@ -0,0 +1,21 @@ +//go:build ignore + +package main + +import ( + "fmt" + + "github.com/fermyon/spin/sdk/go/v2/redis" +) + +func init() { + // redis.Handle() must be called in the init() function. + redis.Handle(func(payload []byte) error { + fmt.Println("Payload::::") + fmt.Println(string(payload)) + return nil + }) +} + +// main functiion must be included for the compiler but is not executed. +func main() {} diff --git a/apps/order-processor/spin.toml b/apps/order-processor/spin.toml new file mode 100644 index 00000000..bf1408c0 --- /dev/null +++ b/apps/order-processor/spin.toml @@ -0,0 +1,20 @@ +spin_manifest_version = 2 + +[application] +name = "order-processor" +version = "0.1.0" +authors = ["Caleb Schoepp "] +description = "Process orders off of redis queue" + +[application.trigger.redis] +address = "redis://:txfM5aXAOe@redis-master.default.svc.cluster.local:6379" + +[[trigger.redis]] +channel = "orders" +component = "order-processor" + +[component.order-processor] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.order-processor.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" diff --git a/apps/variabletester/.gitignore b/apps/variabletester/.gitignore new file mode 100644 index 00000000..b5650104 --- /dev/null +++ b/apps/variabletester/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/apps/variabletester/Dockerfile b/apps/variabletester/Dockerfile new file mode 100644 index 00000000..55ade21a --- /dev/null +++ b/apps/variabletester/Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=${BUILDPLATFORM} tinygo/tinygo:0.30.0 AS build +WORKDIR /opt/build + +COPY go.mod go.sum . + +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +COPY . . + +RUN tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go + +FROM scratch +COPY --from=build /opt/build/main.wasm . +COPY --from=build /opt/build/spin.toml . diff --git a/apps/variabletester/go.mod b/apps/variabletester/go.mod new file mode 100644 index 00000000..c2267345 --- /dev/null +++ b/apps/variabletester/go.mod @@ -0,0 +1,7 @@ +module github.com/variabletester + +go 1.20 + +require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021 + +require github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/apps/variabletester/go.sum b/apps/variabletester/go.sum new file mode 100644 index 00000000..a8bc640a --- /dev/null +++ b/apps/variabletester/go.sum @@ -0,0 +1,4 @@ +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021 h1:btVvhHStqDFm7or/+CKIwcJbOYLy8Co/U/qs2D6DYM8= +github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/apps/variabletester/main.go b/apps/variabletester/main.go new file mode 100644 index 00000000..dbdd9b2e --- /dev/null +++ b/apps/variabletester/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/v2/http" + "github.com/fermyon/spin/sdk/go/v2/variables" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + greetee, err := variables.Get("greetee") + if err != nil { + fmt.Fprintf(w, "err: %s\n", err) + return + } + + fmt.Fprintf(w, "Hello %s!\n", greetee) + }) +} + +func main() {} diff --git a/apps/variabletester/spin.toml b/apps/variabletester/spin.toml new file mode 100644 index 00000000..3ebf893a --- /dev/null +++ b/apps/variabletester/spin.toml @@ -0,0 +1,25 @@ +spin_manifest_version = 2 + +[application] +name = "variabletester" +version = "0.1.0" +authors = ["Danielle Lancashire "] +description = "" + +[[trigger.http]] +route = "/..." +component = "variabletester" + +[variables] +greetee = { required = true } + +[component.variabletester] +source = "main.wasm" +allowed_outbound_hosts = [] + +[component.variabletester.variables] +greetee = "{{ greetee }}" + +[component.variabletester.build] +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" +watch = ["**/*.go", "go.mod"] diff --git a/charts/spin-operator/.helmignore b/charts/spin-operator/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/spin-operator/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/spin-operator/Chart.lock b/charts/spin-operator/Chart.lock new file mode 100644 index 00000000..50cc8bcd --- /dev/null +++ b/charts/spin-operator/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: kwasm-operator + repository: http://kwasm.sh/kwasm-operator + version: 0.2.3 +- name: cert-manager + repository: https://charts.jetstack.io + version: v1.13.3 +digest: sha256:656e3b0c5aadd5694ec9271cb9d257a619ab0ce6453466f61de060346144dd91 +generated: "2024-01-25T13:44:43.972708-07:00" diff --git a/charts/spin-operator/Chart.yaml b/charts/spin-operator/Chart.yaml new file mode 100644 index 00000000..a345dce9 --- /dev/null +++ b/charts/spin-operator/Chart.yaml @@ -0,0 +1,32 @@ +apiVersion: v2 +name: spin-operator +description: A Helm chart for Kubernetes +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" + +dependencies: + - name: kwasm-operator + version: "0.2.3" + repository: "http://kwasm.sh/kwasm-operator" + + - name: cert-manager + repository: https://charts.jetstack.io + condition: certmanager.enabled + alias: certmanager + version: "v1.13.3" diff --git a/charts/spin-operator/README.md b/charts/spin-operator/README.md new file mode 100644 index 00000000..7702a412 --- /dev/null +++ b/charts/spin-operator/README.md @@ -0,0 +1,89 @@ +# spin-operator + +spin-operator is a Kubernetes operator in charge of handling the lifecycle of Spin applications based on their SpinApp resources. + +## Prerequisites + +- Kubernetes v1.11.3+ + +## Prepare the cluster + +Prior to installing the chart, you'll need to ensure the following: + +- spin-operator CustomResourceDefinition (CRD) resources are installed. This includes the SpinApp CRD representing Spin applications to be scheduled on the cluster. + + + + ```console + $ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml + ``` + +- A RuntimeClass resource for the `wasmtime-spin-v2` container runtime is installed. This is the runtime that Spin applications use. + + + + ```console + $ kubectl apply -f - < + +```console +$ helm install spin-operator --namespace spin-operator oci://ghcr.io/spinkube/spin-operator +``` + +## Upgrading the chart + +Note that you may also need to upgrade the spin-operator CRDs in tandem with upgrading the Helm release: + +```console +$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml +``` + +To upgrade the `spin-operator` release, run the following: + +```console +$ helm upgrade spin-operator --namespace spin-operator oci://ghcr.io/spinkube/spin-operator +``` + +## Uninstalling the chart + +To delete the `spin-operator` release, run: + +```console +$ helm delete spin-operator --namespace spin-operator +``` + +This will remove all Kubernetes resources associated with the chart and deletes the Helm release. + +To completely uninstall all resources related to spin-operator, you may want to delete the corresponding CRD resources and, optionally, the RuntimeClass: + +```console +$ kubectl delete -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml + +$ kubectl delete runtimeclass wasmtime-spin-v2 +``` + + diff --git a/charts/spin-operator/templates/NOTES.txt b/charts/spin-operator/templates/NOTES.txt new file mode 100644 index 00000000..b4b59d29 --- /dev/null +++ b/charts/spin-operator/templates/NOTES.txt @@ -0,0 +1,39 @@ +spin-operator {{ .Chart.Version }} is now deployed! + +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm --namespace {{ .Release.Namespace }} status {{ .Release.Name }} + $ helm --namespace {{ .Release.Namespace }} get all {{ .Release.Name }} + +Note: spin-operator requires a few additional resources to be present on the +Kubernetes cluster before it can run the first Spin application. + +1. Install the spin-operator CustomResourceDefinition (CRD) resources: + + {{ if eq .Values.controllerManager.manager.image.tag "latest" }} + $ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml + {{ else }} + $ kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v{{ .Chart.AppVersion }}/spin-operator.crds.yaml + {{ end }} + +2. Install the wasmtime-spin-v2 RuntimeClass: + + {{ if eq .Values.controllerManager.manager.image.tag "latest" }} + $ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.runtime-class.yaml + {{ else }} + $ kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v{{ .Chart.AppVersion }}/spin-operator.runtime-class.yaml + {{ end }} + +3. Finally, install the containerd wasm shim on at least one node. This shim is +necessary for running Spin application workloads. We use the Kwasm Operator +to handle this installation, via node annotations. + + $ kubectl annotate node kwasm.sh/kwasm-node=true + +You are now ready to deploy your first Spin app! + +For further details, see this chart's README: + + $ helm show readme oci://ghcr.io/spinkube/spin-operator diff --git a/charts/spin-operator/templates/_helpers.tpl b/charts/spin-operator/templates/_helpers.tpl new file mode 100644 index 00000000..3ba9a00c --- /dev/null +++ b/charts/spin-operator/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "spin-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "spin-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "spin-operator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "spin-operator.labels" -}} +helm.sh/chart: {{ include "spin-operator.chart" . }} +{{ include "spin-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "spin-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "spin-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "spin-operator.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "spin-operator.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/spin-operator/templates/deployment.yaml b/charts/spin-operator/templates/deployment.yaml new file mode 100644 index 00000000..e018305f --- /dev/null +++ b/charts/spin-operator/templates/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "spin-operator.fullname" . }}-controller-manager + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + control-plane: controller-manager + {{- include "spin-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.controllerManager.replicas }} + selector: + matchLabels: + control-plane: controller-manager + {{- include "spin-operator.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + control-plane: controller-manager + {{- include "spin-operator.selectorLabels" . | nindent 8 }} + annotations: + kubectl.kubernetes.io/default-container: manager + spec: + containers: + - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + command: + - /manager + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag + | default .Chart.AppVersion }} + livenessProbe: + httpGet: + path: /healthz + port: 8082 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8082 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 + }} + securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext + | nindent 10 }} + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - args: {{- toYaml .Values.controllerManager.kubeRbacProxy.args | nindent 8 }} + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag + | default .Chart.AppVersion }} + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent + 10 }} + securityContext: {{- toYaml .Values.controllerManager.kubeRbacProxy.containerSecurityContext + | nindent 10 }} + securityContext: + runAsNonRoot: true + serviceAccountName: {{ include "spin-operator.fullname" . }}-controller-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert \ No newline at end of file diff --git a/charts/spin-operator/templates/leader-election-rbac.yaml b/charts/spin-operator/templates/leader-election-rbac.yaml new file mode 100644 index 00000000..7e19cbbb --- /dev/null +++ b/charts/spin-operator/templates/leader-election-rbac.yaml @@ -0,0 +1,59 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "spin-operator.fullname" . }}-leader-election-role + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "spin-operator.fullname" . }}-leader-election-rolebinding + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ include "spin-operator.fullname" . }}-leader-election-role' +subjects: +- kind: ServiceAccount + name: '{{ include "spin-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/spin-operator/templates/manager-rbac.yaml b/charts/spin-operator/templates/manager-rbac.yaml new file mode 100644 index 00000000..7b7951e4 --- /dev/null +++ b/charts/spin-operator/templates/manager-rbac.yaml @@ -0,0 +1,101 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "spin-operator.fullname" . }}-manager-role + labels: + {{- include "spin-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments/status + verbs: + - get +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/finalizers + verbs: + - update +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/status + verbs: + - get + - patch + - update +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "spin-operator.fullname" . }}-manager-rolebinding + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "spin-operator.fullname" . }}-manager-role' +subjects: +- kind: ServiceAccount + name: '{{ include "spin-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/spin-operator/templates/metrics-reader-rbac.yaml b/charts/spin-operator/templates/metrics-reader-rbac.yaml new file mode 100644 index 00000000..9d454f9a --- /dev/null +++ b/charts/spin-operator/templates/metrics-reader-rbac.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "spin-operator.fullname" . }}-metrics-reader + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get \ No newline at end of file diff --git a/charts/spin-operator/templates/metrics-service.yaml b/charts/spin-operator/templates/metrics-service.yaml new file mode 100644 index 00000000..6a879058 --- /dev/null +++ b/charts/spin-operator/templates/metrics-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "spin-operator.fullname" . }}-controller-manager-metrics-service + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + control-plane: controller-manager + {{- include "spin-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.metricsService.type }} + selector: + control-plane: controller-manager + {{- include "spin-operator.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.metricsService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/charts/spin-operator/templates/mutating-webhook-configuration.yaml b/charts/spin-operator/templates/mutating-webhook-configuration.yaml new file mode 100644 index 00000000..eea65c76 --- /dev/null +++ b/charts/spin-operator/templates/mutating-webhook-configuration.yaml @@ -0,0 +1,29 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "spin-operator.fullname" . }}-mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "spin-operator.fullname" . }}-serving-cert + labels: + {{- include "spin-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "spin-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-core-spinoperator-dev-v1-spinapp + failurePolicy: Fail + name: mspinapp.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinapps + sideEffects: None \ No newline at end of file diff --git a/charts/spin-operator/templates/proxy-rbac.yaml b/charts/spin-operator/templates/proxy-rbac.yaml new file mode 100644 index 00000000..53353be8 --- /dev/null +++ b/charts/spin-operator/templates/proxy-rbac.yaml @@ -0,0 +1,40 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "spin-operator.fullname" . }}-proxy-role + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "spin-operator.fullname" . }}-proxy-rolebinding + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ include "spin-operator.fullname" . }}-proxy-role' +subjects: +- kind: ServiceAccount + name: '{{ include "spin-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/spin-operator/templates/selfsigned-issuer.yaml b/charts/spin-operator/templates/selfsigned-issuer.yaml new file mode 100644 index 00000000..d125addf --- /dev/null +++ b/charts/spin-operator/templates/selfsigned-issuer.yaml @@ -0,0 +1,11 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "spin-operator.fullname" . }}-selfsigned-issuer + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "1" + labels: + {{- include "spin-operator.labels" . | nindent 4 }} +spec: + selfSigned: {} \ No newline at end of file diff --git a/charts/spin-operator/templates/serviceaccount.yaml b/charts/spin-operator/templates/serviceaccount.yaml new file mode 100644 index 00000000..a8460dca --- /dev/null +++ b/charts/spin-operator/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "spin-operator.fullname" . }}-controller-manager + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} \ No newline at end of file diff --git a/charts/spin-operator/templates/serving-cert.yaml b/charts/spin-operator/templates/serving-cert.yaml new file mode 100644 index 00000000..7fe199b9 --- /dev/null +++ b/charts/spin-operator/templates/serving-cert.yaml @@ -0,0 +1,19 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "spin-operator.fullname" . }}-serving-cert + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "2" + labels: + {{- include "spin-operator.labels" . | nindent 4 }} +spec: + dnsNames: + - '{{ include "spin-operator.fullname" . }}-webhook-service.{{ .Release.Namespace + }}.svc' + - '{{ include "spin-operator.fullname" . }}-webhook-service.{{ .Release.Namespace + }}.svc.{{ .Values.kubernetesClusterDomain }}' + issuerRef: + kind: Issuer + name: '{{ include "spin-operator.fullname" . }}-selfsigned-issuer' + secretName: webhook-server-cert \ No newline at end of file diff --git a/charts/spin-operator/templates/validating-webhook-configuration.yaml b/charts/spin-operator/templates/validating-webhook-configuration.yaml new file mode 100644 index 00000000..6b7d83a2 --- /dev/null +++ b/charts/spin-operator/templates/validating-webhook-configuration.yaml @@ -0,0 +1,29 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ include "spin-operator.fullname" . }}-validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "spin-operator.fullname" . }}-serving-cert + labels: + {{- include "spin-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "spin-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-core-spinoperator-dev-v1-spinapp + failurePolicy: Fail + name: vspinapp.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinapps + sideEffects: None \ No newline at end of file diff --git a/charts/spin-operator/templates/webhook-service.yaml b/charts/spin-operator/templates/webhook-service.yaml new file mode 100644 index 00000000..01426eca --- /dev/null +++ b/charts/spin-operator/templates/webhook-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "spin-operator.fullname" . }}-webhook-service + labels: + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + {{- include "spin-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.webhookService.type }} + selector: + control-plane: controller-manager + {{- include "spin-operator.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.webhookService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/charts/spin-operator/values.yaml b/charts/spin-operator/values.yaml new file mode 100644 index 00000000..dda54448 --- /dev/null +++ b/charts/spin-operator/values.yaml @@ -0,0 +1,63 @@ +certmanager: + enabled: true + installCRDs: true +controllerManager: + kubeRbacProxy: + args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: + repository: gcr.io/kubebuilder/kube-rbac-proxy + tag: v0.15.0 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + manager: + args: + - --health-probe-bind-address=:8082 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + - --enable-webhooks + containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + image: + repository: ghcr.io/spinkube/spin-operator + tag: latest + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + replicas: 1 + serviceAccount: + annotations: {} +kubernetesClusterDomain: cluster.local +metricsService: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + type: ClusterIP +webhookService: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + type: ClusterIP diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..ee6d8f8d --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,145 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "github.com/prometheus/common/version" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/controller" + "github.com/spinkube/spin-operator/internal/webhook" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(spinv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + // TODO: Migrate to using github.com/alecthomas/kong so we can improve CLI interface, use env variables etc. + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var enableWebhooks bool + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8082", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&enableWebhooks, "enable-webhooks", false, "Enable admission webhooks") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + setupLog.Info("flag values", + "metricsAddr", metricsAddr, + "probeAddr", probeAddr, + "enableLeaderElection", enableLeaderElection, + "enableWebhooks", enableWebhooks) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{BindAddress: metricsAddr}, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "90ba2d18.spinoperator.dev", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controller.SpinAppReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SpinApp") + os.Exit(1) + } + if err = (&controller.SpinAppExecutorReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SpinAppExecutor") + os.Exit(1) + } + if enableWebhooks { + if err = webhook.SetupSpinAppWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SpinApp") + os.Exit(1) + } + if err = webhook.SetupSpinAppExecutorWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SpinAppExecutor") + os.Exit(1) + } + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + setupLog.Info("version", "version", version.Version, "branch", version.Branch, "revision", version.Revision, "builddate", version.BuildDate) + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 00000000..1538b202 --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + dnsNames: + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..bebea5a5 --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..cf6f89e8 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/config/crd/bases/core.spinoperator.dev_spinappexecutors.yaml b/config/crd/bases/core.spinoperator.dev_spinappexecutors.yaml new file mode 100644 index 00000000..14795864 --- /dev/null +++ b/config/crd/bases/core.spinoperator.dev_spinappexecutors.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: spinappexecutors.core.spinoperator.dev +spec: + group: core.spinoperator.dev + names: + kind: SpinAppExecutor + listKind: SpinAppExecutorList + plural: spinappexecutors + singular: spinappexecutor + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: SpinAppExecutor is the Schema for the spinappexecutors API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SpinAppExecutorSpec defines the desired state of SpinAppExecutor + type: object + status: + description: SpinAppExecutorStatus defines the observed state of SpinAppExecutor + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.spinoperator.dev_spinapps.yaml b/config/crd/bases/core.spinoperator.dev_spinapps.yaml new file mode 100644 index 00000000..b93dd0b3 --- /dev/null +++ b/config/crd/bases/core.spinoperator.dev_spinapps.yaml @@ -0,0 +1,2162 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: spinapps.core.spinoperator.dev +spec: + group: core.spinoperator.dev + names: + kind: SpinApp + listKind: SpinAppList + plural: spinapps + singular: spinapp + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.readyReplicas + name: Ready Replicas + type: integer + - jsonPath: .spec.executor + name: Executor + type: string + name: v1 + schema: + openAPIV3Schema: + description: SpinApp is the Schema for the spinapps API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SpinAppSpec defines the desired state of SpinApp + properties: + checks: + description: Checks defines health checks that should be used by Kubernetes + to monitor the application. + properties: + liveness: + description: Liveness defines the liveness probe for the application. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet describes a health check that should + be performed using a GET request. + properties: + httpHeaders: + description: HTTPHeaders are headers that should be included + in the health check request. + items: + description: HTTPHealthProbeHeader is an abstraction + around a http header key/value pair. + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + description: Path is the path that should be used when + calling the application for a health check, e.g /healthz. + type: string + required: + - path + type: object + initialDelaySeconds: + default: 10 + description: Number of seconds after the app has started before + liveness probes are initiated. Default 10s. + format: int32 + type: integer + periodSeconds: + default: 10 + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + type: integer + type: object + readiness: + description: Readiness defines the readiness probe for the application. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet describes a health check that should + be performed using a GET request. + properties: + httpHeaders: + description: HTTPHeaders are headers that should be included + in the health check request. + items: + description: HTTPHealthProbeHeader is an abstraction + around a http header key/value pair. + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + description: Path is the path that should be used when + calling the application for a health check, e.g /healthz. + type: string + required: + - path + type: object + initialDelaySeconds: + default: 10 + description: Number of seconds after the app has started before + liveness probes are initiated. Default 10s. + format: int32 + type: integer + periodSeconds: + default: 10 + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + type: integer + type: object + type: object + deploymentAnnotations: + additionalProperties: + type: string + description: DeploymentAnnotations defines annotations to be applied + to the underlying deployment. + type: object + enableAutoscaling: + default: false + description: EnableAutoscaling indicates whether the app is allowed + to autoscale. If true then the operator leaves the replica count + of the underlying deployment to be managed by an external autoscaler + (HPA/KEDA). Replicas cannot be defined if this is enabled. By default + EnableAutoscaling is false. + type: boolean + executor: + description: "Executor controls how this app is executed in the cluster. + \n Defaults to whatever executor is available on the cluster. If + multiple executors are available then the first executor in alphabetical + order will be chosen. If no executors are available then no default + will be set." + type: string + image: + description: Image is the source for this app. + type: string + imagePullSecrets: + description: ImagePullSecrets is a list of references to secrets in + the same namespace to use for pulling the image. + items: + description: LocalObjectReference contains enough information to + let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations defines annotations to be applied to the + underlying pods. + type: object + replicas: + description: Number of replicas to run. + format: int32 + type: integer + resources: + description: Resources defines the resource requirements for this + app. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Limits describes the maximum amount of compute resources + allowed. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. Requests cannot exceed Limits. + type: object + type: object + runtimeConfig: + description: RuntimeConfig defines configuration to be applied at + runtime for this app. + properties: + loadFromSecret: + description: LoadFromSecret is the name of the secret to load + runtime config from. The secret should have a single key named + "runtime-config.toml" that contains the base64 encoded runtime + config. + type: string + type: object + serviceAnnotations: + additionalProperties: + type: string + description: ServiceAnnotations defines annotations to be applied + to the underlying service. + type: object + variables: + description: Variables provide Kubernetes Bindings to Spin App Variables. + items: + description: SpinVar defines a binding between a spin variable and + a static or dynamic value. + properties: + name: + description: Name of the variable to bind. + type: string + value: + description: Value is the static value to bind to the variable. + type: string + valueFrom: + description: ValueFrom is a reference to dynamically bind the + variable to. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + volumeMounts: + description: VolumeMounts defines how volumes are mounted in the underlying + containers. + items: + description: VolumeMount describes a mounting of a Volume within + a container. + properties: + mountPath: + description: Path within the container at which the volume should + be mounted. Must not contain ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts are propagated + from the host to container and the other way around. When + not set, MountPropagationNone is used. This field is beta + in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write otherwise + (false or unspecified). Defaults to false. + type: boolean + subPath: + description: Path within the volume from which the container's + volume should be mounted. Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which the + container's volume should be mounted. Behaves similarly to + SubPath but environment variable references $(VAR_NAME) are + expanded using the container's environment. Defaults to "" + (volume's root). SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + description: Volumes defines the volumes to be mounted in the underlying + pods. + items: + description: Volume represents a named volume in a pod that may + be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'awsElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'fsType is the filesystem type of the volume + that you want to mount. Tip: Ensure that the filesystem + type is supported by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from + compromising the machine' + type: string + partition: + description: 'partition is the partition in the volume that + you want to mount. If omitted, the default is to mount + by volume name. Examples: For volume /dev/sda1, you specify + the partition as "1". Similarly, the volume partition + for /dev/sda is "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'readOnly value true will force the readOnly + setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'volumeID is unique ID of the persistent disk + resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: None, + Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in the + blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the blob + storage + type: string + fsType: + description: fsType is Filesystem type to mount. Must be + a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single blob + disk per storage account Managed: azure managed data + disk (only in managed availability set). defaults to shared' + type: string + readOnly: + description: readOnly Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: readOnly defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that contains + Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'monitors is Required: Monitors is a collection + of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted root, + rather than the full Ceph tree, default is /' + type: string + readOnly: + description: 'readOnly is Optional: Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile is the + path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is reference + to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: 'user is optional: User is the rados user name, + default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'cinder represents a cinder volume attached and + mounted on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'readOnly defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'secretRef is optional: points to a secret + object containing parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: 'volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits used to + set permissions on created files by default. Must be an + octal value between 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and decimal values, + JSON requires decimal values for mode bits. Defaults to + 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items if unspecified, each key-value pair in + the Data field of the referenced ConfigMap will be projected + into the volume as a file whose name is the key and content + is the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in + the ConfigMap, the volume setup will error unless it is + marked optional. Paths must be relative and may not contain + the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used to + set permissions on this file. Must be an octal value + between 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and decimal values, + JSON requires decimal values for mode bits. If not + specified, the volume defaultMode will be used. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of the file + to map the key to. May not be an absolute path. + May not contain the path element '..'. May not start + with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: optional specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents ephemeral + storage that is handled by certain external CSI drivers (Beta + feature). + properties: + driver: + description: driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated + CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: nodePublishSecretRef is a reference to the + secret object containing sensitive information to pass + to the CSI driver to complete the CSI NodePublishVolume + and NodeUnpublishVolume calls. This field is optional, + and may be empty if no secret is required. If the secret + object contains more than one secret, all secret references + are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: readOnly specifies a read-only configuration + for the volume. Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: volumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a Optional: mode bits used to set + permissions on created files by default. Must be an octal + value between 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and decimal values, + JSON requires decimal values for mode bits. Defaults to + 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: 'Optional: mode bits used to set permissions + on this file, must be an octal value between 0000 + and 0777 or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON requires + decimal values for mode bits. If not specified, + the volume defaultMode will be used. This might + be in conflict with other options that affect the + file mode, like fsGroup, and the result can be other + mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path + name of the file to be created. Must not be absolute + or contain the ''..'' path. Must be utf-8 encoded. + The first item of the relative path must not start + with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'emptyDir represents a temporary directory that + shares a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'medium represents what type of storage medium + should back this directory. The default is "" which means + to use the node''s default medium. Must be an empty string + (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: 'sizeLimit is the total amount of local storage + required for this EmptyDir volume. The size limit is also + applicable for memory medium. The maximum usage on memory + medium EmptyDir would be the minimum value between the + SizeLimit specified here and the sum of memory limits + of all containers in a pod. The default is nil which means + that the limit is undefined. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: "ephemeral represents a volume that is handled + by a cluster storage driver. The volume's lifecycle is tied + to the pod that defines it - it will be created before the + pod starts, and deleted when the pod is removed. \n Use this + if: a) the volume is only needed while the pod runs, b) features + of normal volumes like restoring from snapshot or capacity + tracking are needed, c) the storage driver is specified through + a storage class, and d) the storage driver supports dynamic + volume provisioning through a PersistentVolumeClaim (see EphemeralVolumeSource + for more information on the connection between this volume + type and PersistentVolumeClaim). \n Use PersistentVolumeClaim + or one of the vendor-specific APIs for volumes that persist + for longer than the lifecycle of an individual pod. \n Use + CSI for light-weight local ephemeral volumes if the CSI driver + is meant to be used that way - see the documentation of the + driver for more information. \n A pod can use both types of + ephemeral volumes and persistent volumes at the same time." + properties: + volumeClaimTemplate: + description: "Will be used to create a stand-alone PVC to + provision the volume. The pod in which this EphemeralVolumeSource + is embedded will be the owner of the PVC, i.e. the PVC + will be deleted together with the pod. The name of the + PVC will be `-` where `` is the name from the `PodSpec.Volumes` array entry. + Pod validation will reject the pod if the concatenated + name is not valid for a PVC (for example, too long). \n + An existing PVC with that name that is not owned by the + pod will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC + is meant to be used by the pod, the PVC has to updated + with an owner reference to the pod once the pod exists. + Normally this should not be necessary, but it may be useful + when manually reconstructing a broken cluster. \n This + field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. \n Required, must + not be nil." + properties: + metadata: + description: May contain labels and annotations that + will be copied into the PVC when creating it. No other + fields are allowed and will be rejected during validation. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + The entire content is copied unchanged into the PVC + that gets created from this template. The same fields + as in a PersistentVolumeClaim are also valid here. + properties: + accessModes: + description: 'accessModes contains the desired access + modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'dataSource field can be used to specify + either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) If the + provisioner or an external controller can support + the specified data source, it will create a new + volume based on the contents of the specified + data source. When the AnyVolumeDataSource feature + gate is enabled, dataSource contents will be copied + to dataSourceRef, and dataSourceRef contents will + be copied to dataSource when dataSourceRef.namespace + is not specified. If the namespace is specified, + then dataSourceRef will not be copied to dataSource.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, + the specified Kind must be in the core API + group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being + referenced + type: string + name: + description: Name is the name of resource being + referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: 'dataSourceRef specifies the object + from which to populate the volume with data, if + a non-empty volume is desired. This may be any + object from a non-empty API group (non core object) + or a PersistentVolumeClaim object. When this field + is specified, volume binding will only succeed + if the type of the specified object matches some + installed volume populator or dynamic provisioner. + This field will replace the functionality of the + dataSource field and as such if both fields are + non-empty, they must have the same value. For + backwards compatibility, when namespace isn''t + specified in dataSourceRef, both fields (dataSource + and dataSourceRef) will be set to the same value + automatically if one of them is empty and the + other is non-empty. When namespace is specified + in dataSourceRef, dataSource isn''t set to the + same value and must be empty. There are three + important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types + of objects, dataSourceRef allows any non-core + object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping + them), dataSourceRef preserves all values, and + generates an error if a disallowed value is specified. + * While dataSource only allows local objects, + dataSourceRef allows objects in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource + feature gate to be enabled. (Alpha) Using the + namespace field of dataSourceRef requires the + CrossNamespaceVolumeDataSource feature gate to + be enabled.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, + the specified Kind must be in the core API + group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being + referenced + type: string + name: + description: Name is the name of resource being + referenced + type: string + namespace: + description: Namespace is the namespace of resource + being referenced Note that when a namespace + is specified, a gateway.networking.k8s.io/ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept + the reference. See the ReferenceGrant documentation + for details. (Alpha) This field requires the + CrossNamespaceVolumeDataSource feature gate + to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: 'resources represents the minimum resources + the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to specify + resource requirements that are lower than previous + value but must still be higher than capacity recorded + in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum + amount of compute resources required. If Requests + is omitted for a container, it defaults to + Limits if that is explicitly specified, otherwise + to an implementation-defined value. Requests + cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: selector is a label query over volumes + to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement + is a selector that contains values, a key, + and an operator that relates the key and + values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is + "In", and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: 'storageClassName is the name of the + StorageClass required by the claim. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeAttributesClassName: + description: 'volumeAttributesClassName may be used + to set the VolumeAttributesClass used by this + claim. If specified, the CSI driver will create + or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This + has a different purpose than storageClassName, + it can be changed after the claim is created. + An empty string value means that no VolumeAttributesClass + will be applied to the claim but it''s not allowed + to reset this field to empty string once it is + set. If unspecified and the PersistentVolumeClaim + is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller + if it exists. If the resource referred to by volumeAttributesClass + does not exist, this PersistentVolumeClaim will + be set to a Pending state, as reflected by the + modifyVolumeStatus field, until such as a resource + exists. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass + feature gate to be enabled.' + type: string + volumeMode: + description: volumeMode defines what type of volume + is required by the claim. Value of Filesystem + is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that is + attached to a kubelet's host machine and then exposed to the + pod. + properties: + fsType: + description: 'fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. TODO: how do we prevent errors in the + filesystem from compromising the machine' + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'readOnly is Optional: Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: 'wwids Optional: FC volume world wide identifiers + (wwids) Either wwids or combination of targetWWNs and + lun must be set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: flexVolume represents a generic volume resource + that is provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use for + this volume. + type: string + fsType: + description: fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends + on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds extra + command options if any.' + type: object + readOnly: + description: 'readOnly is Optional: defaults to false (read/write). + ReadOnly here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'secretRef is Optional: secretRef is reference + to the secret object containing sensitive information + to pass to the plugin scripts. This may be empty if no + secret object is specified. If the secret object contains + more than one secret, all secrets are passed to the plugin + scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached to + a kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: datasetName is Name of the dataset stored as + metadata -> name on the dataset for Flocker should be + considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. This + is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'gcePersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'fsType is filesystem type of the volume that + you want to mount. Tip: Ensure that the filesystem type + is supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from + compromising the machine' + type: string + partition: + description: 'partition is the partition in the volume that + you want to mount. If omitted, the default is to mount + by volume name. Examples: For volume /dev/sda1, you specify + the partition as "1". Similarly, the volume partition + for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'pdName is unique name of the PD resource in + GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'gitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision + a container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: directory is the target directory name. Must + not contain or start with '..'. If '.' is supplied, the + volume directory will be the git repository. Otherwise, + if specified, the volume will contain the git repository + in the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'glusterfs represents a Glusterfs mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'endpoints is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'readOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to + false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'hostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'path of the directory on the host. If the + path is a symlink, it will follow the link to the real + path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'iscsi represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to + the pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support iSCSI + Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support iSCSI + Session CHAP authentication + type: boolean + fsType: + description: 'fsType is the filesystem type of the volume + that you want to mount. Tip: Ensure that the filesystem + type is supported by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from + compromising the machine' + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. If initiatorName is specified with iscsiInterface + simultaneously, new iSCSI interface : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iscsiInterface is the interface Name that uses + an iSCSI transport. Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal List. The + portal is either an IP or ip_addr:port if the port is + other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: readOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI target + and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. The Portal + is either an IP or ip_addr:port if the port is other than + default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'name of the volume. Must be a DNS_LABEL and unique + within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'nfs represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'readOnly here will force the NFS export to + be mounted with read-only permissions. Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'persistentVolumeClaimVolumeSource represents a + reference to a PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: readOnly Will force the ReadOnly setting in + VolumeMounts. Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used to set permissions + on created files by default. Must be an octal value between + 0000 and 0777 or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON requires decimal + values for mode bits. Directories within the path are + not affected by this setting. This might be in conflict + with other options that affect the file mode, like fsGroup, + and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + clusterTrustBundle: + description: "ClusterTrustBundle allows a pod to access + the `.spec.trustBundle` field of ClusterTrustBundle + objects in an auto-updating file. \n Alpha, gated + by the ClusterTrustBundleProjection feature gate. + \n ClusterTrustBundle objects can either be selected + by name, or by the combination of signer name and + a label selector. \n Kubelet performs aggressive + normalization of the PEM contents written into the + pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates + are deduplicated. The ordering of certificates within + the file is arbitrary, and Kubelet may change the + order over time." + properties: + labelSelector: + description: Select all ClusterTrustBundles that + match this label selector. Only has effect + if signerName is set. Mutually-exclusive with + name. If unset, interpreted as "match nothing". If + set but empty, interpreted as "match everything". + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, a + key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and + DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is + "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: Select a single ClusterTrustBundle + by object name. Mutually-exclusive with signerName + and labelSelector. + type: string + optional: + description: If true, don't block pod startup + if the referenced ClusterTrustBundle(s) aren't + available. If using name, then the named ClusterTrustBundle + is allowed not to exist. If using signerName, + then the combination of signerName and labelSelector + is allowed to match zero ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume root + to write the bundle. + type: string + signerName: + description: Select all ClusterTrustBundles that + match this signer name. Mutually-exclusive with + name. The contents of all selected ClusterTrustBundles + will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: items if unspecified, each key-value + pair in the Data field of the referenced ConfigMap + will be projected into the volume as a file + whose name is the key and content is the value. + If specified, the listed keys will be projected + into the specified paths, and unlisted keys + will not be present. If a key is specified which + is not present in the ConfigMap, the volume + setup will error unless it is marked optional. + Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. + Must be an octal value between 0000 and + 0777 or a decimal value between 0 and + 511. YAML accepts both octal and decimal + values, JSON requires decimal values for + mode bits. If not specified, the volume + defaultMode will be used. This might be + in conflict with other options that affect + the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be + an absolute path. May not contain the + path element '..'. May not start with + the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the downwardAPI + data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: 'Optional: mode bits used to + set permissions on this file, must be + an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML + accepts both octal and decimal values, + JSON requires decimal values for mode + bits. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can + be other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' + path. Must be utf-8 encoded. The first + item of the relative path must not start + with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the + container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu + and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the secret data + to project + properties: + items: + description: items if unspecified, each key-value + pair in the Data field of the referenced Secret + will be projected into the volume as a file + whose name is the key and content is the value. + If specified, the listed keys will be projected + into the specified paths, and unlisted keys + will not be present. If a key is specified which + is not present in the Secret, the volume setup + will error unless it is marked optional. Paths + must be relative and may not contain the '..' + path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. + Must be an octal value between 0000 and + 0777 or a decimal value between 0 and + 511. YAML accepts both octal and decimal + values, JSON requires decimal values for + mode bits. If not specified, the volume + defaultMode will be used. This might be + in conflict with other options that affect + the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be + an absolute path. May not contain the + path element '..'. May not start with + the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: optional field specify whether the + Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information about + the serviceAccountToken data to project + properties: + audience: + description: audience is the intended audience + of the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: expirationSeconds is the requested + duration of validity of the service account + token. As the token approaches expiration, the + kubelet volume plugin will proactively rotate + the service account token. The kubelet will + start trying to rotate the token if the token + is older than 80 percent of its time to live + or if the token is older than 24 hours.Defaults + to 1 hour and must be at least 10 minutes. + format: int64 + type: integer + path: + description: path is the path relative to the + mount point of the file to project the token + into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime + properties: + group: + description: group to map volume access to Default is no + group + type: string + readOnly: + description: readOnly here will force the Quobyte volume + to be mounted with read-only permissions. Defaults to + false. + type: boolean + registry: + description: registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: user to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'rbd represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type of the volume + that you want to mount. Tip: Ensure that the filesystem + type is supported by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from + compromising the machine' + type: string + image: + description: 'image is the rados image name. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'pool is the rados pool name. Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'secretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is + nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: 'user is the rados user name. Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: readOnly Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: secretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + description: storageMode indicates whether the storage for + a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool associated + with the protection domain. + type: string + system: + description: system is the name of the storage system as + configured in ScaleIO. + type: string + volumeName: + description: volumeName is the name of a volume already + created in the ScaleIO system that is associated with + this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used to + set permissions on created files by default. Must be an + octal value between 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and decimal values, + JSON requires decimal values for mode bits. Defaults to + 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items If unspecified, each key-value pair in + the Data field of the referenced Secret will be projected + into the volume as a file whose name is the key and content + is the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in + the Secret, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used to + set permissions on this file. Must be an octal value + between 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and decimal values, + JSON requires decimal values for mode bits. If not + specified, the volume defaultMode will be used. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of the file + to map the key to. May not be an absolute path. + May not contain the path element '..'. May not start + with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: 'secretName is the name of the secret in the + pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. Must + be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: secretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: volumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within + a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of the + volume within StorageOS. If no namespace is specified + then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS + for tighter integration. Set VolumeName to any name to + override the default behaviour. Set to "default" if you + are not using namespaces within StorageOS. Namespaces + that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: fsType is filesystem type to mount. Must be + a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy Based + Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies vSphere + volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - executor + - image + - replicas + type: object + status: + description: SpinAppStatus defines the observed state of SpinApp + properties: + activeScheduler: + description: ActiveScheduler is the name of the scheduler that is + currently scheduling this SpinApp. + type: string + conditions: + description: 'Represents the observations of a SpinApps''s current + state. SpinApp.status.conditions.type are: "Available" and "Progressing" + SpinApp.status.conditions.status are one of True, False, Unknown. + SpinApp.status.conditions.reason the value should be a CamelCase + string and producers of specific condition types may define expected + values and meanings for this field, and whether the values are considered + a guaranteed API. SpinApp.status.conditions.Message is a human readable + message indicating details about the transition. For further information + see: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + readyReplicas: + description: Represents the current number of active replicas on the + application deployment. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/spin.fermyon.com_spinapps.yaml b/config/crd/bases/spin.fermyon.com_spinapps.yaml new file mode 100644 index 00000000..58ae2f47 --- /dev/null +++ b/config/crd/bases/spin.fermyon.com_spinapps.yaml @@ -0,0 +1,310 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: spinapps.spin.fermyon.com +spec: + group: spin.fermyon.com + names: + kind: SpinApp + listKind: SpinAppList + plural: spinapps + singular: spinapp + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: SpinApp is the Schema for the spinapps API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SpinAppSpec defines the desired state of SpinApp + properties: + checks: + description: Checks defines health checks that should be used by Kubernetes + to monitor the application. + properties: + liveness: + description: HealthProbe defines an individual health check for + an application. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet describes a health check that should + be performed using a GET request. + properties: + httpHeaders: + description: HTTPHeaders are headers that should be included + in the health check request. + items: + description: HTTPHealthProbeHeader is an abstraction + around a http header key/value pair. + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + description: Path is the path that should be used when + calling the application for a health check, e.g /healthz. + type: string + required: + - path + type: object + initialDelaySeconds: + description: Number of seconds after the app has started before + liveness probes are initiated. + format: int32 + type: integer + periodSeconds: + default: 10 + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + type: integer + type: object + readiness: + description: HealthProbe defines an individual health check for + an application. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet describes a health check that should + be performed using a GET request. + properties: + httpHeaders: + description: HTTPHeaders are headers that should be included + in the health check request. + items: + description: HTTPHealthProbeHeader is an abstraction + around a http header key/value pair. + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + description: Path is the path that should be used when + calling the application for a health check, e.g /healthz. + type: string + required: + - path + type: object + initialDelaySeconds: + description: Number of seconds after the app has started before + liveness probes are initiated. + format: int32 + type: integer + periodSeconds: + default: 10 + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + type: integer + type: object + type: object + deploymentAnnotations: + additionalProperties: + type: string + description: DeploymentAnnotations defines annotations to be applied + to the underlying deployment. + type: object + image: + description: Image is the source for this app. + type: string + imagePullSecrets: + description: ImagePullSecrets is a list of references to secrets in + the same namespace to use for pulling the image. + items: + description: LocalObjectReference contains enough information to + let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations defines annotations to be applied to the + underlying pods. + type: object + replicas: + description: Number of replicas to run. + format: int32 + type: integer + runtime: + default: containerd-shim-spin + description: Runtime is the runtime to use for this app. Defaults + to "containerd-shim-spin". + enum: + - containerd-shim-spin + - cyclotron + type: string + runtimeConfig: + description: RuntimeConfig defines configuration to be applied at + runtime for this app. + properties: + loadFromSecret: + description: LoadFromSecret is the name of the secret to load + runtime config from. The secret should have a single key named + "runtime-config.toml" that contains the base64 encoded runtime + config. + type: string + type: object + serviceAnnotations: + additionalProperties: + type: string + description: ServiceAnnotations defines annotations to be applied + to the underlying service. + type: object + required: + - image + - replicas + - runtime + type: object + status: + description: SpinAppStatus defines the observed state of SpinApp + properties: + active: + description: A list of pointers to currently running spin apps. + items: + description: "ObjectReference contains enough information to let + you inspect or modify the referred object. --- New uses of this + type are discouraged because of difficulty describing its usage + when embedded in APIs. 1. Ignored fields. It includes many fields + which are not generally honored. For instance, ResourceVersion + and FieldPath are both very rarely valid in actual usage. 2. Invalid + usage help. It is impossible to add specific help for individual + usage. In most embedded usages, there are particular restrictions + like, \"must refer only to types A and B\" or \"UID not honored\" + or \"name must be restricted\". Those cannot be well described + when embedded. 3. Inconsistent validation. Because the usages + are different, the validation rules are different by usage, which + makes it hard for users to predict what will happen. 4. The fields + are both imprecise and overly precise. Kind is not a precise + mapping to a URL. This can produce ambiguity during interpretation + and require a REST mapping. In most cases, the dependency is + on the group,resource tuple and the version of the actual struct + is irrelevant. 5. We cannot easily change it. Because this type + is embedded in many locations, updates to this type will affect + numerous schemas. Don't make new APIs embed an underspecified + API type they do not control. \n Instead of using this type, create + a locally provided and used type that is well-focused on your + reference. For example, ServiceReferences for admission registration: + https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 + ." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + lastScheduleTime: + description: Information when was the last time the spin app was successfully + scheduled. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 00000000..67a4654f --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,26 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/core.spinoperator.dev_spinapps.yaml +- bases/core.spinoperator.dev_spinappexecutors.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +- path: patches/webhook_in_spinapps.yaml +- path: patches/webhook_in_spinappexecutors.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +- path: patches/cainjection_in_spinapps.yaml +- path: patches/cainjection_in_spinappexecutors.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000..ec5c150a --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/crd/patches/cainjection_in_spinappexecutors.yaml b/config/crd/patches/cainjection_in_spinappexecutors.yaml new file mode 100644 index 00000000..3c9339d1 --- /dev/null +++ b/config/crd/patches/cainjection_in_spinappexecutors.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: spinappexecutors.core.spinoperator.dev diff --git a/config/crd/patches/cainjection_in_spinapps.yaml b/config/crd/patches/cainjection_in_spinapps.yaml new file mode 100644 index 00000000..9bfef85e --- /dev/null +++ b/config/crd/patches/cainjection_in_spinapps.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: spinapps.core.spinoperator.dev diff --git a/config/crd/patches/webhook_in_spinappexecutors.yaml b/config/crd/patches/webhook_in_spinappexecutors.yaml new file mode 100644 index 00000000..9dd1fb89 --- /dev/null +++ b/config/crd/patches/webhook_in_spinappexecutors.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: spinappexecutors.core.spinoperator.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_spinapps.yaml b/config/crd/patches/webhook_in_spinapps.yaml new file mode 100644 index 00000000..886c7247 --- /dev/null +++ b/config/crd/patches/webhook_in_spinapps.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: spinapps.core.spinoperator.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 00000000..b52fd43c --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,142 @@ +# Adds namespace to all resources. +namespace: spin-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: spin-operator- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patches: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- path: manager_auth_proxy_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +replacements: + - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldPath: .metadata.namespace # namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - select: + kind: CustomResourceDefinition + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - source: # Add cert-manager annotation to the webhook Service + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true + - source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000..b48c09f3 --- /dev/null +++ b/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,40 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8082" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" + - "--enable-webhooks" diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml new file mode 100644 index 00000000..f6f58916 --- /dev/null +++ b/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..738de350 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 00000000..5f56b799 --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,29 @@ +# This patch add annotation to admission webhook config and +# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: mutatingwebhookconfiguration + app.kubernetes.io/instance: mutating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 00000000..5c5f0b84 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 00000000..25839fa4 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,103 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + - --enable-webhooks + image: ghcr.io/spinkube/spin-operator:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8082 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8082 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 00000000..ed137168 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 00000000..5411a472 --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,25 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 00000000..d186c256 --- /dev/null +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000..89da5f4b --- /dev/null +++ b/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000..e83b9003 --- /dev/null +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000..2a7b652a --- /dev/null +++ b/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 00000000..731832a6 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 00000000..c00d6973 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,44 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000..00c8f33a --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..f4711719 --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments/status + verbs: + - get +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/finalizers + verbs: + - update +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/status + verbs: + - get + - patch + - update +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 00000000..bf3c078a --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 00000000..b341956d --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/config/rbac/spinapp_editor_role.yaml b/config/rbac/spinapp_editor_role.yaml new file mode 100644 index 00000000..4b1efef4 --- /dev/null +++ b/config/rbac/spinapp_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit spinapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: spinapp-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: spinapp-editor-role +rules: +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps/status + verbs: + - get diff --git a/config/rbac/spinapp_viewer_role.yaml b/config/rbac/spinapp_viewer_role.yaml new file mode 100644 index 00000000..6b6649c7 --- /dev/null +++ b/config/rbac/spinapp_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view spinapps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: spinapp-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: spinapp-viewer-role +rules: +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps + verbs: + - get + - list + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinapps/status + verbs: + - get diff --git a/config/rbac/spinappexecutor_editor_role.yaml b/config/rbac/spinappexecutor_editor_role.yaml new file mode 100644 index 00000000..49eb9867 --- /dev/null +++ b/config/rbac/spinappexecutor_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit spinappexecutors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: spinappexecutor-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: spinappexecutor-editor-role +rules: +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/status + verbs: + - get diff --git a/config/rbac/spinappexecutor_viewer_role.yaml b/config/rbac/spinappexecutor_viewer_role.yaml new file mode 100644 index 00000000..21753655 --- /dev/null +++ b/config/rbac/spinappexecutor_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view spinappexecutors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: spinappexecutor-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: spinappexecutor-viewer-role +rules: +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors + verbs: + - get + - list + - watch +- apiGroups: + - core.spinoperator.dev + resources: + - spinappexecutors/status + verbs: + - get diff --git a/config/samples/annotations.yaml b/config/samples/annotations.yaml new file mode 100644 index 00000000..31a3f8c9 --- /dev/null +++ b/config/samples/annotations.yaml @@ -0,0 +1,15 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: annotations-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin + serviceAnnotations: + key: value + deploymentAnnotations: + key: value + multiple-keys: are-supported + podAnnotations: + key: value diff --git a/config/samples/cyclotron.yaml b/config/samples/cyclotron.yaml new file mode 100644 index 00000000..64aca211 --- /dev/null +++ b/config/samples/cyclotron.yaml @@ -0,0 +1,8 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: cyclotron-spinapp +spec: + image: "public.ecr.aws/m8p2l5g2/cyclotron-demo-apps:hello" + replicas: 1 + executor: "cyclotron" diff --git a/config/samples/hpa.yaml b/config/samples/hpa.yaml new file mode 100644 index 00000000..1f571b49 --- /dev/null +++ b/config/samples/hpa.yaml @@ -0,0 +1,35 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: hpa-spinapp +spec: + # TODO: Depend on a ghcr.io version of this image + image: "ttl.sh/cpu-load-gen:1h" + executor: containerd-shim-spin + enableAutoscaling: true + resources: + limits: + cpu: 500m + memory: 500Mi + requests: + cpu: 100m + memory: 400Mi +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: spinapp-autoscaler +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: hpa-spinapp + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 00000000..e2341c1d --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,15 @@ +## Append samples of your project ## +resources: +- annotations.yaml +- cyclotron.yaml +- hpa.yaml +- private-image.yaml +- probes.yaml +- redis.yaml +- resources.yaml +- runtime-config.yaml +- shim-executor.yaml +- simple.yaml +- variables.yaml +- volume-mount.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/private-image.yaml b/config/samples/private-image.yaml new file mode 100644 index 00000000..e810bbc2 --- /dev/null +++ b/config/samples/private-image.yaml @@ -0,0 +1,12 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: private-image-spinapp +spec: + image: "ghcr.io//:" + # For testing, you can create a secret with the following command: + # kubectl create secret docker-registry spin-image-secret --docker-server=https://ghcr.io --docker-username=$YOUR_GITHUB_USERNAME --docker-password=$YOUR_GITHUB_PERSONAL_ACCESS_TOKEN --docker-email=$YOUR_EMAIL + imagePullSecrets: + - name: spin-image-secret + replicas: 1 + executor: containerd-shim-spin \ No newline at end of file diff --git a/config/samples/probes.yaml b/config/samples/probes.yaml new file mode 100644 index 00000000..46f2409c --- /dev/null +++ b/config/samples/probes.yaml @@ -0,0 +1,15 @@ +apiVersion: spinoperator.dev/v1 +kind: SpinApp +metadata: + name: healthchecks-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin + checks: + liveness: + httpGet: + path: "/hello" + readiness: + httpGet: + path: "/go-hello" diff --git a/config/samples/redis.yaml b/config/samples/redis.yaml new file mode 100644 index 00000000..86d613d1 --- /dev/null +++ b/config/samples/redis.yaml @@ -0,0 +1,9 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: redis-spinapp +spec: + image: "ttl.sh/caleb-redis-thing-2:24h" + replicas: 1 + executor: containerd-shim-spin +# Steps to run this found at https://github.com/spinkube/spin-operator/pull/131 \ No newline at end of file diff --git a/config/samples/resources.yaml b/config/samples/resources.yaml new file mode 100644 index 00000000..90824522 --- /dev/null +++ b/config/samples/resources.yaml @@ -0,0 +1,15 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: resource-requirements-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin + resources: + limits: + cpu: 500m + memory: 500Mi + requests: + cpu: 100m + memory: 400Mi diff --git a/config/samples/runtime-config.yaml b/config/samples/runtime-config.yaml new file mode 100644 index 00000000..c3b41d45 --- /dev/null +++ b/config/samples/runtime-config.yaml @@ -0,0 +1,18 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: runtime-config-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin + runtimeConfig: + loadFromSecret: runtime-config +--- +apiVersion: v1 +kind: Secret +metadata: + name: runtime-config +type: Opaque +data: + runtime-config.toml: bG9nX2RpciA9ICIvYXNkZiIK diff --git a/config/samples/shim-executor.yaml b/config/samples/shim-executor.yaml new file mode 100644 index 00000000..19d6068f --- /dev/null +++ b/config/samples/shim-executor.yaml @@ -0,0 +1,5 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinAppExecutor +metadata: + name: containerd-shim-spin +spec: diff --git a/config/samples/simple.yaml b/config/samples/simple.yaml new file mode 100644 index 00000000..fcbdbadc --- /dev/null +++ b/config/samples/simple.yaml @@ -0,0 +1,8 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: simple-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin diff --git a/config/samples/variables.yaml b/config/samples/variables.yaml new file mode 100644 index 00000000..dba5c21c --- /dev/null +++ b/config/samples/variables.yaml @@ -0,0 +1,11 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: variables-spinapp +spec: + image: "ghcr.io/endocrimes/spin-variabletester:container" + replicas: 1 + executor: containerd-shim-spin + variables: + - name: greetee + value: Fermyon diff --git a/config/samples/volume-mount.yaml b/config/samples/volume-mount.yaml new file mode 100644 index 00000000..c0e60fbd --- /dev/null +++ b/config/samples/volume-mount.yaml @@ -0,0 +1,42 @@ +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: volume-mount-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + executor: containerd-shim-spin + volumes: + - name: example-volume + persistentVolumeClaim: + claimName: example-pv-claim + volumeMounts: + - name: example-volume + mountPath: "/mnt/data" +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: example-pv-claim +spec: + storageClassName: manual + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: pv-volume + labels: + type: local +spec: + storageClassName: manual + capacity: + storage: 100Mi + accessModes: + - ReadWriteOnce + hostPath: + path: "/mnt/data" \ No newline at end of file diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..9cf26134 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..206316e5 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..db5dba1c --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-core-spinoperator-dev-v1-spinapp + failurePolicy: Fail + name: mspinapp.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinapps + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-core-spinoperator-dev-v1-spinappexecutor + failurePolicy: Fail + name: mspinappexecutor.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinappexecutors + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-spinoperator-dev-v1-spinapp + failurePolicy: Fail + name: vspinapp.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinapps + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-spinoperator-dev-v1-spinappexecutor + failurePolicy: Fail + name: vspinappexecutor.kb.io + rules: + - apiGroups: + - core.spinoperator.dev + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - spinappexecutors + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..c5026c0e --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: spin-operator + app.kubernetes.io/part-of: spin-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/documentation/content/contributing.md b/documentation/content/contributing.md new file mode 100644 index 00000000..ad5c9abf --- /dev/null +++ b/documentation/content/contributing.md @@ -0,0 +1,7 @@ +# Contributing + +Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for a guide on how to contribute to this project. + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) diff --git a/documentation/content/custom-resource-definition-reference.md b/documentation/content/custom-resource-definition-reference.md new file mode 100644 index 00000000..bd1940ce --- /dev/null +++ b/documentation/content/custom-resource-definition-reference.md @@ -0,0 +1,119 @@ +- [Custom Resource Definition (CRD) Reference](#custom-resource-definition-crd-reference) + - [Runtime (Deprecated)](#runtime-deprecated) + - [Executor (Optional)](#executor-optional) + - [Scheduler](#scheduler) + - [Image (Required)](#image-required) + - [ImagePullSecrets (Optional)](#imagepullsecrets-optional) + - [Replicas (Required)](#replicas-required) + - [RuntimeConfig (Optional)](#runtimeconfig-optional) + - [ServiceAnnotations (Optional)](#serviceannotations-optional) + - [DeploymentAnnotations (Optional)](#deploymentannotations-optional) + - [PodAnnotations (Optional)](#podannotations-optional) + - [ResourceRequirements](#resourcerequirements) + - [VolumeMounts](#volumemounts) + - [Status](#status) + +# Custom Resource Definition (CRD) Reference + +## Runtime (Deprecated) + +## Executor (Optional) + +Executor configures what will execute the `SpinApp`. Currently we support `containerd-shim-spin`. However, this field is configurable so Spin Operator can manage other runtimes in the future. + +If you choose `containerd-shim-spin` (it is also the default) the operator will create a deployment and a service. The service will point at pods managed by the deployment. The deployment will start and manage the pods with `containerd-shim-spin` for you. It is expected that you already have the `wasmtime-spin-v2` runtime class installed on the cluster to use this executor. + +See [this sample application](https://github.com/spinkube/spin-operator/blob/main/config/samples/cyclotron.yaml). + +## Scheduler + +The Spin Operator Scheduler is currently operating based on one instance per scheduler. For now, Spin Operator is treated as being necessary to control SpinApps. Spin Operator can manage a Service regardless of the scheduler. + +We have the option of making a future feature whereby the Service management is configurable on a per-scheduler basis. This allows external schedulers to define their own. + +## Image (Required) + +Points to the image of the Spin app you want to run. For example: + +```yaml +image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" +``` + +See [this sample application](https://github.com/spinkube/spin-operator/blob/main/config/samples/simple.yaml) + +## ImagePullSecrets (Optional) + +In some cases, your image might be coming from a private registry. Lets you reference a k8s secret that has credentials for you to pull an image. + +For example, a secret which is created with the following command: + +```bash +kubectl create secret docker-registry spin-image-secret --docker-server=https://ghcr.io --docker-username=$YOUR_GITHUB_USERNAME --docker-password=$YOUR_GITHUB_PERSONAL_ACCESS_TOKEN --docker-email=$YOUR_EMAIL +``` + +See [this sample application](https://github.com/spinkube/spin-operator/blob/main/config/samples/private-image.yaml) + +## Replicas (Required) + +Replicas is a field in the `SpinApp` Custom Resource Definition (CRD). Configures how many replicas of a spin app you want to run. If containerd-shim-spin is the executor that is the number of pods. This definition may very depending on how other executors choose to define replica. + +## RuntimeConfig (Optional) + +Lets you define Spin runtime config for your app. You must base64 encode your runtime config and put it in a secret with the right key. See the sample app for an example. It will then put the runtime config as a volume mount in your pod in the right place for the shim to pick it up and use it. + +Converts a runtime-config.toml file to a Kubernetes secret `runtime-config-to-secret [PATH_TO_RUNTIME_CONFIG] [SECRET_NAME]` + +See [this sample application](https://github.com/spinkube/spin-operator/blob/main/config/samples/runtime-config.yaml) + +## ServiceAnnotations (Optional) + +Passing annotations through to the deployment is supported. Lets you set specific annotations on the underlying service that is created. For example: + +```yaml +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: annotations-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + serviceAnnotations: + key: value + deploymentAnnotations: + key: value + multiple-keys: are-supported + podAnnotations: + key: value +``` + +See [this example application](https://github.com/spinkube/spin-operator/blob/main/config/samples/annotations.yaml) + +## DeploymentAnnotations (Optional) + +Lets you set specific annotations on the underlying deployment that is created. + +See [this example application](https://github.com/spinkube/spin-operator/blob/main/config/samples/annotations.yaml) + +## PodAnnotations (Optional) + +Lets you set specific annotations on the underlying pods that are created. + +See [this example application](https://github.com/spinkube/spin-operator/blob/main/config/samples/annotations.yaml) + +## ResourceRequirements + +Still being developed TODO + +## VolumeMounts + +Still being developed TODO + +## Status + +Still being designed TODO + +The status field indicates the state of a `SpinApp` in the Kubernetes cluster which includes the state of the resources the `SpinApp` controller has created. Below are examples of status: + +``` +TODO +``` diff --git a/documentation/content/deploying-with-helm.md b/documentation/content/deploying-with-helm.md new file mode 100644 index 00000000..73ea887c --- /dev/null +++ b/documentation/content/deploying-with-helm.md @@ -0,0 +1,100 @@ +- [Deploying with Helm](#deploying-with-helm) + - [Prerequisites](#prerequisites) + - [Install Helm](#install-helm) + - [Install Spin Operator Using Helm](#install-spin-operator-using-helm) + - [Prepare the Cluster](#prepare-the-cluster) + - [Installing the Chart](#installing-the-chart) + - [Upgrading the Chart](#upgrading-the-chart) + - [Uninstalling the Chart](#uninstalling-the-chart) + +# Deploying with Helm + +## Prerequisites + +Please ensure that your system has all of the [./prerequisites.md](prerequisites) installed before continuing. + +## Install Spin Operator Using Helm + +The following instructions are for installing Spin Operator as a chart (using helm install). + +### Prepare the Cluster + +Before installing the chart, you'll need to ensure the following: + +The [Custom Resource Definition (CRD)](glossary-of-terms#custom-resource-definition-crd) resources are installed. This includes the SpinApp CRD representing Spin applications to be scheduled on the cluster. + + + +```console +$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml +``` + +A [RuntimeClass](glossary-of-terms/#runtime-class) resource for the `wasmtime-spin-v2` container runtime is installed. This is the runtime that Spin applications use. + + + +```console +$ kubectl apply -f - < + +```console +$ helm install spin-operator --namespace spin-operator oci://ghcr.io/spinkube/spin-operator +``` + +### Upgrading the Chart + +Note that you may also need to upgrade the spin-operator CRDs in tandem with upgrading the Helm release: + +```console +$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml +``` + +To upgrade the `spin-operator` release, run the following: + +```console +$ helm upgrade spin-operator --namespace spin-operator oci://ghcr.io/spinkube/spin-operator +``` + +### Uninstalling the Chart + +To delete the `spin-operator` release, run: + +```console +$ helm delete spin-operator --namespace spin-operator +``` + +This will remove all Kubernetes resources associated with the chart and deletes the Helm release. + +To completely uninstall all resources related to spin-operator, you may want to delete the corresponding CRD resources and, optionally, the RuntimeClass: + +```console +$ kubectl delete -f https://github.com/spinkube/spin-operator/releases/latest/download/spin-operator.crds.yaml + +$ kubectl delete runtimeclass wasmtime-spin-v2 +``` + + diff --git a/documentation/content/glossary-of-terms.md b/documentation/content/glossary-of-terms.md new file mode 100644 index 00000000..d9394319 --- /dev/null +++ b/documentation/content/glossary-of-terms.md @@ -0,0 +1,103 @@ +- [Glossary of Terms](#glossary-of-terms) + - [Chart](#chart) + - [Cluster](#cluster) + - [Container Runtime](#container-runtime) + - [Controller](#controller) + - [Custom Resource (CR)](#custom-resource-cr) + - [Custom Resource Definition (CRD)](#custom-resource-definition-crd) + - [Helm](#helm) + - [Image](#image) + - [Kubernetes (K8s)](#kubernetes-k8s) + - [Open Container Initiative (OCI)](#open-container-initiative-oci) + - [Pod](#pod) + - [Role Based Access Control (RBAC)](#role-based-access-control-rbac) + - [Runtime Class](#runtime-class) + - [Scheduler](#scheduler) + - [Service](#service) + - [Spin](#spin) + - [Spin Operator](#spin-operator) + +# Glossary of Terms + +The following glossary of terms is in the context of deploying, scaling, automating and managing Spin applications in containerized environments. + +## Chart + +Helm packages are known as "charts". The main Spin Operator chart does not include the SpinApp CRD or any non-namespace or cluster-level resources. + +## Cluster + +TODO + +## Container Runtime + +TODO + +## Controller + +TODO + +## Custom Resource (CR) + +A CR facilitates the storage and retrieval of your own API Objects (as structured data). A Spin application can be described as a CR. + +## Custom Resource Definition (CRD) + +A CRD defines your Custom Resources (CR). For example, the following `.yaml` file describes a `SpinApp` using CRD syntax: + +```yaml +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: simple-spinapp +spec: + image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0" + replicas: 1 + runtime: "containerd-shim-spin" +``` + +> SpinApp CRDs are kept separate from Helm. If using Helm, CustomResourceDefinition (CRD) resources will need to be installed prior to installing the Heml chart. + +## Helm + +TODO + +## Image + +TODO + +## Kubernetes (K8s) + +TODO + +## Open Container Initiative (OCI) + +TODO + +## Pod + +A pod is a group of containers that can share resources. + +## Role Based Access Control (RBAC) + +TODO + +## Runtime Class + +A RuntimeClass isn't a namespaced resource. A RuntimeClass is not part of a Helm chart. + +## Scheduler + +TODO + +## Service + +In Kubernetes, a Service is an abstraction that defines a logical set of Pods that enables clients to interact with a consistent set of Pods, regardless of whether the code is designed for a cloud-native environment or a containerized legacy application. + +## Spin + +Spin is a framework designed for building and running event-driven microservice applications using WebAssembly (Wasm) components. + +## Spin Operator + +Spin Operator is a Kubernetes (K8s) operator in charge of handling the lifecycle of Spin applications based on their SpinApp resources. diff --git a/documentation/content/integrations.md b/documentation/content/integrations.md new file mode 100644 index 00000000..8c813e38 --- /dev/null +++ b/documentation/content/integrations.md @@ -0,0 +1,23 @@ +- [Integrations](#integrations) + - [KEDA](#keda) + - [HPA](#hpa) + - [Gateway API](#gateway-api) + - [Dapr](#dapr) + +# Integrations + +## KEDA + +TODO - Still being experimented with / designed / developed. See #100 for latest status. + +## HPA + +TODO - Still being experimented with / designed / developed. See #57 for latest status. + +## Gateway API + +TODO - Still being experimented with / designed / developed. See #76 for the latest status. + +## Dapr + +TODO - Still being experimented with / designed / developed. See #77 for latest status. diff --git a/documentation/content/operator_development.md b/documentation/content/operator_development.md new file mode 100644 index 00000000..bdf7b272 --- /dev/null +++ b/documentation/content/operator_development.md @@ -0,0 +1,129 @@ +# Spin Operator Development + +- [To Deploy on the Cluster](#to-deploy-on-the-cluster) +- [To Uninstall](#to-uninstall) +- [Packaging and Deployment via Helm](#packaging-and-deployment-via-helm) + +## To Deploy on the Cluster + +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/spin-operator:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Apply the Runtime Class to the cluster:** + +```sh +k apply -f config/samples/runtime-class.yaml +``` + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy cert-manager to the cluster:** + +```sh +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml +``` + +> **NOTE**: Cert-manager is required to manage the TLS certificates for the admission webhooks. + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/spin-operator:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +> privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +> **NOTE**: Ensure that the samples has default values to test it out. + +## To Uninstall + +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**Delete the Runtime Class from the cluster:** + +```sh +k delete -f config/samples/runtime-class.yaml +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +**UnDeploy cert-manager from the cluster:** + +```sh +kubectl remove -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml +``` + +### Packaging and deployment via Helm + +The [Spin Operator chart](./charts/spin-operator) is assembled via a combination of +[helmify](https://github.com/arttor/helmify) using the kustomize manifests from the +[config](./config/) directory as well as other non-kustomize items such as the +[NOTES.txt](./charts/spin-operator/templates/NOTES.txt) and [Chart.yaml](./charts/spin-operator/Chart.yaml). + +> **NOTE**: Manual changes to helmify-generated resources, including the +> [values.yml](./charts/spin-operator/values.yaml) file and applicable resources in +> [templates](./charts/spin-operator/templates/) are not persisted across helmify +> invocations. + +**Generate the Helm chart:** + +```sh +make helm-generate +``` + +**Install the Helm chart onto the cluster:** + +> **Note**: [CRDs](./config/crd/bases/) and the [wasm-spin-v2](./config/samples/runtime-class.yaml) +> RuntimeClass are currently _not_ installed as part of the chart. You'll need to ensure these are +> present via the method(s) mentioned above. + +```sh +make helm-install +``` + +Follow the release notes printed after helm installs the chart for next steps. + +**Upgrade the Helm release on the cluster:** + +```sh +make helm-upgrade +``` + +**Delete the Helm release from the cluster:** + +```sh +make helm-uninstall +``` diff --git a/documentation/content/prerequisites.md b/documentation/content/prerequisites.md new file mode 100644 index 00000000..4d7f18ba --- /dev/null +++ b/documentation/content/prerequisites.md @@ -0,0 +1,40 @@ +- [Prerequisites](#prerequisites) + - [Go](#go) + - [TinyGo](#tinygo) + - [Docker](#docker) + - [Kubectl](#kubectl) + - [K3d](#k3d) + - [Helm](#helm) + - [Bombardier](#bombardier) + +# Prerequisites + +The following prerequisites are required. + +## Go + +If building the Spin Operator from source or contributing to the development of Spin Operator then you will require [Go](https://go.dev/doc/install) version v1.21.0+ to be installed on your machine. Otherwise, please ignore this section, and move to the next prerequisite. + +### TinyGo + +Please also install the latest version of [TinyGo](https://tinygo.org/getting-started/install/) + +## Docker + +If you'd like to run Spin Operator locally, then please install [Docker](https://docs.docker.com/get-docker/) version 17.03+. + +## Kubectl + +If you'd like to manage your Spin applications with `kubectl`, then Spin Operator requires that you have [kubectl](https://kubernetes.io/docs/tasks/tools/) version v1.27.0+ installed. + +## K3d + +If running/deploying your Spin application involves the use of k3d, then the Spin Operator requires that you have [k3d](https://k3d.io/v5.6.0/?h=installation#installation) installed and that you have access to a Kubernetes v1.27.0 cluster. + +## Helm + +If running/deploying your Spin application involves the use of Helm, then the Spin Operator requires that you have [Helm](https://helm.sh/docs/intro/install/#helm) installed on your system. + +## Bombardier + +Installing [Bombardier](https://pkg.go.dev/github.com/codesenberg/bombardier) is **not** required to use Spin Operator. Bombardier is used in tutorials like [Scaling Spinapps on k8s With HPA](./scaling-spinapp-on-k8s-with-hpa.md) to generate load to test autoscaling. diff --git a/documentation/content/project-goals.md b/documentation/content/project-goals.md new file mode 100644 index 00000000..e9e5b0a0 --- /dev/null +++ b/documentation/content/project-goals.md @@ -0,0 +1,8 @@ +- [Project Goals](#project-goals) + +# Project Goals + +The goals of this project are: + +- Decide if we can and should use KubeBuilder to build/support the SpinApp CRD and Operator. +- Use this as a proof of concept for deploying Spin apps. diff --git a/documentation/content/project-overview.md b/documentation/content/project-overview.md new file mode 100644 index 00000000..28f61554 --- /dev/null +++ b/documentation/content/project-overview.md @@ -0,0 +1,5 @@ +- [Spin Operator](#spin-operator) + +# Project Overview + +The spin operator watches Spin App Custom Resources and realizes the desired state in the Kubernetes cluster. This is an experimental version of the operator to be used in the spin-gateway project. This project was built using the [kubebuilder](https://book.kubebuilder.io/) framework and contains a Spin App CRD and controller. diff --git a/documentation/content/quickstart.md b/documentation/content/quickstart.md new file mode 100644 index 00000000..525e6d4a --- /dev/null +++ b/documentation/content/quickstart.md @@ -0,0 +1,41 @@ +- [Quickstart](#quickstart) + +# Quickstart + +TODO +Helm plus the `kubectl apply` for CRDs is recommended. Documentation coming soon. + +## Prerequisites + +Ensure necessary [prerequisites](./prerequisites.md) are installed. + +### Setting Up Your Kubernetes Cluster + +1. Create a Kubernetes k3d cluster that has containerd-wasm-shim pre-requistes installed: + +``` +k3d cluster create wasm-cluster --image ghcr.io/deislabs/containerd-wasm-shims/examples/k3d:v0.10.0 -p "8081:80@loadbalancer" --agents 2 +``` + +2. Apply the Runtime Class: + +``` +kubectl apply -f spin-runtime-class.yaml +``` + +## Running the Sample Application + +1. `make install` to install the SpinApp CRD on to the cluster +2. `make run` to build and run the controller locally +3. In a different terminal window: `kubectl apply -f config/samples/shim-executor.yaml` +4. `kubectl apply -f config/samples/simple.yaml` +5. `kubectl port-forward svc/simple-spinapp 8083:80` +6. In a different terminal window: `curl localhost:8083/hello` + +You should see: + +```bash +Hello world from Spin! +``` + +If you want to test the admission webhooks you'll need to follow the instructions [here](operator_development.md) to deploy the operator to the cluster. We disable webhooks when using `make run` because that would require us to locally setup TLS certificates. diff --git a/documentation/content/running-locally.md b/documentation/content/running-locally.md new file mode 100644 index 00000000..d5cd7b0d --- /dev/null +++ b/documentation/content/running-locally.md @@ -0,0 +1,107 @@ +- [Running Locally](#running-locally) + - [Prerequisites](#prerequisites) + - [Fetch Spin Operator (Source Code)](#fetch-spin-operator-source-code) + - [Setting Up Kubernetes Cluster](#setting-up-kubernetes-cluster) + - [Installation With Make](#installation-with-make) + - [Running the Sample Application](#running-the-sample-application) + +# Running Locally + +## Prerequisites + +Please ensure that your system has all of the [prerequisites](./prerequisites.md) installed before continuing. + +## Fetch Spin Operator (Source Code) + +Clone the Spin Operator repository: + +```bash +git clone https://github.com/spinkube/spin-operator.git +``` + +Change into the Spin Operator directory: + +```bash +cd spin-operator +``` + +## Setting Up Kubernetes Cluster + +Run the following command to create a Kubernetes k3d cluster that has [the containerd-wasm-shims](https://github.com/deislabs/containerd-wasm-shims) pre-requisites installed: + +```bash +k3d cluster create wasm-cluster --image ghcr.io/deislabs/containerd-wasm-shims/examples/k3d:v0.10.0 -p "8081:80@loadbalancer" --agents 2 +``` + +Run the following command to create the Runtime Class: + +```bash +kubectl apply -f - < 80 +Forwarding from [::1]:8083 -> 80 +``` + +Run the following command, in a different terminal window: + +```bash +curl localhost:8083/hello +``` + +The above command will return the following message: + +```bash +Hello world from Spin! +``` diff --git a/documentation/content/running-on-a-cluster.md b/documentation/content/running-on-a-cluster.md new file mode 100644 index 00000000..0d4b58bf --- /dev/null +++ b/documentation/content/running-on-a-cluster.md @@ -0,0 +1,32 @@ +- [Running On a Cluster](#running-on-a-cluster) + - [Prerequisites](#prerequisites) + - [Running on Your Kubernetes Cluster](#running-on-your-kubernetes-cluster) + +# Running On a Cluster + +## Prerequisites + +Please ensure that your system has all of the [./prerequisites.md](prequisites) installed before continuing. + +## Running on Your Kubernetes Cluster + +This is the standard development workflow for when you want to test running Spin Operator on a Kubernetes cluster. This is harder than [running Spin Operator on your local machine](./running-locally.md), but deploying Spin Operator into your cluster lets you test things like webhooks. + +> Note that you need to [install cert-manager](https://cert-manager.io/docs/installation/) for webhook support. + +Deploy the Manager to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/spin-operator:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +> privileges or be logged in as admin. + +To create instances of your solution, apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +> **NOTE**: Ensure that the samples has default values to test it out. diff --git a/documentation/content/scaling-spinapp-on-k8s-with-hpa.md b/documentation/content/scaling-spinapp-on-k8s-with-hpa.md new file mode 100644 index 00000000..8854ab4b --- /dev/null +++ b/documentation/content/scaling-spinapp-on-k8s-with-hpa.md @@ -0,0 +1,199 @@ +- [Scaling Spinapp on Kubernetes (k8s) With Horizontal Pod Autoscaling (HPA)](#scaling-spinapp-on-kubernetes-k8s-with-horizontal-pod-autoscaling-hpa) + - [Prerequisites](#prerequisites) + - [Fetch Spin Operator (Source Code)](#fetch-spin-operator-source-code) + - [Setting Up k8s Cluster](#setting-up-k8s-cluster) + - [Set Up Ingress](#set-up-ingress) + - [Build and Store Spinapp in a TTL Registry](#build-and-store-spinapp-in-a-ttl-registry) + - [Deploy SpinApp and HPA](#deploy-spinapp-and-hpa) + - [Generate Load to Test Autoscale](#generate-load-to-test-autoscale) + +# Scaling Spinapp on Kubernetes (k8s) With Horizontal Pod Autoscaling (HPA) + +Horizontal scaling, in the k8s sense, means deploying more pods to meet demand (different from vertical scaling whereby more memory and CPU resources are assigned to already running pods). In this tutorial, we configure [HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) to dynamically scale the instance count of our SpinApps to meet the demand. + +## Prerequisites + +> We use k3d to run a k8s cluster locally as part of this tutorial, but you can follow these steps to configure HPA autoscaling on your desired k8s environment. + +Please see the [Go](./prerequisites.md#go), [Docker](./prerequisites.md#docker), [Kubectl](./prerequisites.md#kubectl), [k3d](./prerequisites.md#k3d) and [Bombardier](#prerequisites#bombardier) sections in the [Prerequisites](./prerequisites.md) page and fulfill those prerequisite requirements before continuing. + +## Fetch Spin Operator (Source Code) + +If you haven't already, please go ahead and clone the Spin Operator repository: + +```bash +git clone https://github.com/spinkube/spin-operator.git +``` + +Change into the Spin Operator directory: + +```bash +cd spin-operator +``` + +## Setting Up k8s Cluster + +Run the following command to create a k8s k3d cluster that has [the containerd-wasm-shims](https://github.com/deislabs/containerd-wasm-shims) pre-requisites installed: If you have a k3d cluster already, please feel free to use it: + +```sql +k3d cluster create wasm-cluster-scale --image ghcr.io/deislabs/containerd-wasm-shims/examples/k3d:v0.10.0 -p "8081:80@loadbalancer" --agents 2 +``` + +Next, from within the `spin-operator` directory, run the following commands to install the Spin runtime class and Spin Operator: + +```sql +kubectl apply -f spin-runtime-class.yaml +make install +``` + +Lastly, start the operator with the following command: + +```sql +make run +``` + +Great, now you have Spin Operator up and running on your cluster. This means you’re set to create and deploy SpinApps later on in the tutorial. + +## Set Up Ingress + +Use the following command to set up ingress on your k8s cluster. This ensures traffic can reach your SpinApp once we’ve created it in future steps: + +```bash +# Setup ingress following this tutorialhttps://k3d.io/v5.4.6/usage/exposing_services/ +cat <nginx-ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + annotations: + ingress.kubernetes.io/ssl-redirect: "false" +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: hpa-spinapp + port: + number: 80 +EOF +``` + +Hit enter to create the ingress resource. It can live inside the `config/samples` directory alongside other sample applications. + +## Build and Store Spinapp in an OCI Registry + +Next up we’re going to build the SpinApp we will be scaling and storing inside of a [ttl.sh](http://ttl.sh) registry. Change into the [apps/cpu-load-gen](https://github.com/spinkube/spin-operator/tree/hpa-tutorial/apps/cpu-load-gen) directory and build the SpinApp we’ve provided: + +```bash +# Build and publish the sample app +cd apps/cpu-load-gen +spin build +spin registry push ttl.sh/cpu-load-gen:1h +``` + +Note that the tag at the end of [ttl.sh/cpu-load-gen:1h](http://ttl.sh/cpu-load-gen:1h) indicates how long the image will last e.g. `1h` (1 hour). The maximum is `24h` and you will need to repush if ttl exceeds 24 hours. + + + +## Deploy SpinApp and HPA + +We can take a look at the SpinApp and HPA definitions in our deployment file below/. As you can see, we have set our `resources` -> `limits` to `500m` of `cpu` and `500Mi` of `memory` per Spin application and we will scale the instance count when we’ve reached a 50% utilization in `cpu` and `memory`. We’ve also defined support a maximum [replica](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#replicas) count of 10 and a minimum replica count of 1: + +```yaml +apiVersion: core.spinoperator.dev/v1 +kind: SpinApp +metadata: + name: hpa-spinapp +spec: + # TODO: Depend on a ghcr.io version of this image + image: "ttl.sh/cpu-load-gen:1h" + enableAutoscaling: true + resources: + limits: + cpu: 500m + memory: 500Mi + requests: + cpu: 100m + memory: 400Mi +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: spinapp-autoscaler +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: hpa-spinapp + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 +``` + + + +The k8s documentation is the place to learn more about [limits and requests](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits) and [other metrics supported by HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#container-resource-metrics). + +Let’s deploy the SpinApp and the HPA instance onto our cluster with the following command: + +```bash +kubectl apply -f config/samples/hpa.yaml +``` + +You can see your running Spin application by running the following command: + +```bash +kubectl get spinapps +NAME AGE +hpa-spinapp 92m +``` + +You can also see your HPA instance with the following command: + +```bash +NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE +spinapp-autoscaler Deployment/hpa-spinapp 6%/50% 1 10 1 97m +``` + +## Generate Load to Test Autoscale + +Now let’s use Bombardier to generate traffic to test how well HPA scales our SpinApp. The following Bombardier command will attempt to establish 40 connections during a period of 3 minutes (or less). If a request is not responded to within 5 seconds that request will timeout: + +```bash +# Generate a bunch of load +bombardier -c 40 -t 5s -d 3m http://localhost:8081 +``` + +To watch the load, we can run the following command to get the status of our deployment: + +```bash +kubectl describe deploy hpa-spinapp +... +--- + +Available True MinimumReplicasAvailable +Progressing True NewReplicaSetAvailable +OldReplicaSets: +NewReplicaSet: hpa-spinapp-544c649cf4 (1/1 replicas created) +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal ScalingReplicaSet 11m deployment-controller Scaled up replica set hpa-spinapp-544c649cf4 to 1 + Normal ScalingReplicaSet 9m45s deployment-controller Scaled up replica set hpa-spinapp-544c649cf4 to 4 + Normal ScalingReplicaSet 9m30s deployment-controller Scaled up replica set hpa-spinapp-544c649cf4 to 8 + Normal ScalingReplicaSet 9m15s deployment-controller Scaled up replica set hpa-spinapp-544c649cf4 to 10 +``` diff --git a/documentation/content/share-images.md b/documentation/content/share-images.md new file mode 100644 index 00000000..3234e13a --- /dev/null +++ b/documentation/content/share-images.md @@ -0,0 +1,14 @@ +- [Share Images](#share-images) + - [To Deploy on the Cluster](#to-deploy-on-the-cluster) + +# Share Images + +## To Deploy on the Cluster + +Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/spin-operator:tag +``` + +**NOTE:** This image ought to be published in the personal registry you specified. And it is required to have access to pull the image from the working environment. Make sure you have the proper permission to the registry if the above commands don’t work. diff --git a/documentation/content/troubleshooting.md b/documentation/content/troubleshooting.md new file mode 100644 index 00000000..572e775e --- /dev/null +++ b/documentation/content/troubleshooting.md @@ -0,0 +1,100 @@ +- [Troubleshooting](#troubleshooting) + - [Cluster Already Exists](#cluster-already-exists) + - [Cluster Information](#cluster-information) + - [Cluster Delete](#cluster-delete) + - [Too long: must have at most 262144 bytes](#too-long-must-have-at-most-262144-bytes) + - [Redis Operator](#redis-operator) + - [error: requires go version](#error-requires-go-version) + +# Troubleshooting + +The following is a list of common error messages and potential troubleshooting suggestions that might assist you with your work. + +## Cluster Already Exists + +When trying to create a cluster (e.g. a cluster named `wasm-cluster`) you may recieve an error message similar to the following: + +```bash +FATA[0000] Failed to create cluster 'wasm-cluster' because a cluster with that name already exists +``` + +### Cluster Information + +With `k3d` installed, you can use the following command to get a cluster list: + +```bash +$ k3d cluster list +NAME SERVERS AGENTS LOADBALANCER +wasm-cluster 1/1 2/2 true +``` + +With `kubectl installed, you can use the following command to dump cluster information (this is much more verbose): + +```bash +$ kubectl cluster-info dump +``` + +### Cluster Delete + +With `k3d` installed, you can delete the cluster by name, as shown in the command below: + +```bash +$ k3d cluster delete wasm-cluster +INFO[0000] Deleting cluster 'wasm-cluster' +INFO[0002] Deleting cluster network 'k3d-wasm-cluster' +INFO[0002] Deleting 1 attached volumes... +INFO[0002] Removing cluster details from default kubeconfig... +INFO[0002] Removing standalone kubeconfig file (if there is one)... +INFO[0002] Successfully deleted cluster wasm-cluster! +``` + +## Too long: must have at most 262144 bytes + +When running `kubectl apply -f my-file.yaml`, the following error can occur of the `.yam.` file is too large: + +```bash +Too long: must have at most 262144 bytes +``` + +Using the `--server-side=true` option resolves this issue: + +```bash +kubectl apply --server-side=true -f my-file.yaml +``` + +## Redis Operator + +Noted an error when installing Redis Operator: + +```bash +$ helm repo add redis-operator https://spotahome.github.io/redis-operator +"redis-operator" has been added to your repositories +$ helm repo update +Hang tight while we grab the latest from your chart repositories... +...Successfully got an update from the "redis-operator" chart repository +Update Complete. ⎈Happy Helming!⎈ +$ helm install redis-operator redis-operator/redis-operator +Error: INSTALLATION FAILED: failed to install CRD crds/databases.spotahome.com_redisfailovers.yaml: error parsing : error converting YAML to JSON: yaml: line 4: did not find expected node content +``` + +Used the following commands to enforce using a different version of Redis Operator (whilst waiting on [this PR fix](https://github.com/spotahome/redis-operator/pull/685) to be merged). + +```bash +$ helm install redis-operator redis-operator/redis-operator --version 3.2.9 +NAME: redis-operator +LAST DEPLOYED: Mon Jan 22 12:33:54 2024 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +``` + +## error: requires go version + +When building apps like the [cpu-load-gen](../../apps/cpu-load-gen/) Spin app, you may get the following error if your TinyGo is not up to date. The error requires go version `1.18` through `1.20` but this is not necessarily the case. It **is** recommended that you have the latest go installed e.g. `1.21` and downgrading is not necessary. Instead please go ahead and [install the latest version of TinyGo](./prerequisites.md#tinygo) to resolve this error: + +```bash +user@user:~/spin-operator/apps/cpu-load-gen$ spin build +Building component cpu-load-gen with `tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go` +error: requires go version 1.18 through 1.20, got go1.21 +``` diff --git a/documentation/content/uninstall.md b/documentation/content/uninstall.md new file mode 100644 index 00000000..22ccbfd2 --- /dev/null +++ b/documentation/content/uninstall.md @@ -0,0 +1,36 @@ +- [Uninstall](#uninstall) + - [Delete (CRs)](#delete-crs) + - [Delete APIs(CRDs)](#delete-apiscrds) + - [UnDeploy](#undeploy) + +# Uninstall + +These are commands to delete, uninstall and undeploy resources. + +## Delete (CRs) + +The following command will delete the instances (CRs) from the cluster: + +```bash +kubectl delete -k config/samples/ +``` + +## Delete APIs(CRDs) + +The following command will uninstall CRDs from the K8s cluster specified in `~/.kube/config`: + +```sh +make uninstall +``` + +> Call with `ignore-not-found=true` to ignore resource not found errors during deletion. + +## UnDeploy + +The following command will undeploy the controller from the K8s cluster specified in `~/.kube/config`: + +```sh +make undeploy +``` + +> Call with `ignore-not-found=true` to ignore resource not found errors during deletion. diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..ba3b7537 --- /dev/null +++ b/flake.lock @@ -0,0 +1,124 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs-format", + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1637475807, + "narHash": "sha256-E3nzOvlzZXwyo8Stp5upKsTCDcqUTYAFj4EC060A31c=", + "owner": "nix-community", + "repo": "fenix", + "rev": "960e7fef45692a4fffc6df6d6b613b0399bbdfd5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1705856552, + "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs-format": { + "inputs": { + "fenix": "fenix", + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1705307188, + "narHash": "sha256-2UDso6ALCoqVH0Q0boIYRT9NJtto8CECAc+gUIHi1/o=", + "owner": "nix-community", + "repo": "nixpkgs-fmt", + "rev": "7301bc9f2ba29fe693c04cbcaa12110eb9685c71", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs-fmt", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-format": "nixpkgs-format" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1637439871, + "narHash": "sha256-2awQ/obzl7zqYgLwbQL0zT58gN8Xq7n+81GcMiS595I=", + "owner": "rust-analyzer", + "repo": "rust-analyzer", + "rev": "4566414789310acb2617543f4b50beab4bb48e06", + "type": "github" + }, + "original": { + "owner": "rust-analyzer", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..3b2bd255 --- /dev/null +++ b/flake.nix @@ -0,0 +1,40 @@ +{ + description = "The Spin Operator for Kubernetes"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + + nixpkgs-format.url = "github:nix-community/nixpkgs-fmt"; + nixpkgs-format.inputs.nixpkgs.follows = "nixpkgs"; + nixpkgs-format.inputs.flake-utils.follows = "flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, nixpkgs-format }@inputs: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + buildDeps = with pkgs; [ + go_1_21 + gnumake + git + ]; + + devDeps = with pkgs; buildDeps ++ [ + gopls + kubectl + kubernetes-helm + gotestsum + golangci-lint + ]; + in + { + devShells.default = pkgs.mkShell { + buildInputs = devDeps ++ [ + nixpkgs-format.defaultPackage.${system} + ]; + }; + }); +} diff --git a/format.Dockerfile b/format.Dockerfile new file mode 100644 index 00000000..677e4490 --- /dev/null +++ b/format.Dockerfile @@ -0,0 +1,12 @@ +# Dockerfile to run prettier on markdown in the codebase. Used by lint-markdown +# and lint-markdown-fix rules in Makefile. + +FROM node:alpine + +WORKDIR /usr/spin-operator + +RUN npm install prettier -g + +ENV PRETTIER_MODE=check + +CMD sh -c "npx prettier --${PRETTIER_MODE} '**/*.md'" diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..8a11ba0f --- /dev/null +++ b/go.mod @@ -0,0 +1,71 @@ +module github.com/spinkube/spin-operator + +go 1.21 + +toolchain go1.21.5 + +require ( + github.com/go-logr/logr v1.4.1 + github.com/prometheus/common v0.46.0 + github.com/stretchr/testify v1.8.4 + k8s.io/api v0.29.1 + k8s.io/apimachinery v0.29.1 + k8s.io/client-go v0.29.1 + sigs.k8s.io/controller-runtime v0.17.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..2e582739 --- /dev/null +++ b/go.sum @@ -0,0 +1,197 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= +sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 00000000..65b86227 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/hack/provision-minikube.sh b/hack/provision-minikube.sh new file mode 100755 index 00000000..ea8bbd77 --- /dev/null +++ b/hack/provision-minikube.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Provision a new minikube instance configured with the containerd shim for Spin. + +echo "Starting minikube" +minikube start --container-runtime containerd + +echo "Installing the containerd shim" +curl -fsSLO https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.10.0/containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz +tar -zxvf containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz + +echo "Copying the shim to minikube" +minikube cp containerd-shim-spin-v2 /usr/local/bin/ +minikube ssh sudo chmod +x /usr/local/bin/containerd-shim-spin-v2 + +# just cleaning up +rm containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz containerd-shim-spin-v2 + +echo "Configuring containerd" +if ! minikube ssh -- grep -q io.containerd.spin /etc/containerd/config.toml; then + echo nope + minikube ssh 'cat << EOF | sudo tee -a /etc/containerd/config.toml +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.spin] + runtime_type = "io.containerd.spin.v2" +EOF' +fi + +echo "Restarting containerd" +minikube ssh sudo systemctl restart containerd + +echo "Creating runtime class" +kubectl apply -f - <"${secret_name}.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: $secret_name +type: Opaque +data: + runtime-config.toml: $encoded_content +EOF + +echo "Kubernetes secret created at ${secret_name}.yaml" \ No newline at end of file diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 00000000..0c6d53e8 --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,22 @@ +package constants + +import "fmt" + +// OperatorResourceKeyspace is the keyspace used for constructing application +// metadata on Kubernetes objects +const OperatorResourceKeyspace = "core.spinoperator.dev" + +// ConstructResourceLabelKey is used when building operator-managed labels for +// resources. +func ConstructResourceLabelKey(kind string) string { + return fmt.Sprintf("%s/%s", OperatorResourceKeyspace, kind) +} + +// KnownExecutor is an enumeration of the executors that are well-known and +// supported by the spin operator. +type KnownExecutor string + +const ( + ContainerDShimSpinExecutor = "containerd-shim-spin" + CyclotronExecutor = "cyclotron" +) diff --git a/internal/controller/deployment.go b/internal/controller/deployment.go new file mode 100644 index 00000000..f14c9107 --- /dev/null +++ b/internal/controller/deployment.go @@ -0,0 +1,152 @@ +package controller + +import ( + "context" + "errors" + "fmt" + "strings" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/pkg/spinapp" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func constructRuntimeConfigSecretMount(_ctx context.Context, secretName string) (corev1.Volume, corev1.VolumeMount) { + volume := corev1.Volume{ + Name: "spin-runtime-config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + Optional: ptr(true), + Items: []corev1.KeyToPath{ + { + Key: "runtime-config.toml", + Path: "runtime-config.toml", + }, + }, + }, + }, + } + volumeMount := corev1.VolumeMount{ + Name: "spin-runtime-config", + ReadOnly: true, + MountPath: "/runtime-config.toml", + SubPath: "runtime-config.toml", + } + + return volume, volumeMount +} + +// ConstructVolumeMountsForApp introspects the application and generates +// any required volume mounts. A generated runtime secret is mutually +// exclusive with a user-provided secret - this is to require _either_ a +// manual runtime-config or a generated one from the crd. +func ConstructVolumeMountsForApp(ctx context.Context, app *spinv1.SpinApp, generatedRuntimeSecret string) ([]corev1.Volume, []corev1.VolumeMount, error) { + volumes := []corev1.Volume{} + volumeMounts := []corev1.VolumeMount{} + + userProvidedRuntimeSecret := app.Spec.RuntimeConfig.LoadFromSecret + if userProvidedRuntimeSecret != "" && generatedRuntimeSecret != "" { + return nil, nil, errors.New("cannot specify both a user-provided runtime secret and a generated one") + } + + selectedSecret := userProvidedRuntimeSecret + if generatedRuntimeSecret != "" { + selectedSecret = generatedRuntimeSecret + } + + if selectedSecret != "" { + runtimeConfigVolume, runtimeConfigMount := constructRuntimeConfigSecretMount(ctx, selectedSecret) + volumes = append(volumes, runtimeConfigVolume) + volumeMounts = append(volumeMounts, runtimeConfigMount) + } + + // TODO: Once #49 lands validate that volumes don't start with `spin-` prefix in admission webhook. + volumes = append(volumes, app.Spec.Volumes...) + volumeMounts = append(volumeMounts, app.Spec.VolumeMounts...) + + return volumes, volumeMounts, nil +} + +// ConstructEnvForApp constructs the env for a spin app that runs as a k8s pod. +// Variables are not guaranteed to stay backed by ENV. +func ConstructEnvForApp(ctx context.Context, app *spinv1.SpinApp) []corev1.EnvVar { + if len(app.Spec.Variables) == 0 { + return nil + } + + envs := make([]corev1.EnvVar, len(app.Spec.Variables)) + for idx, variable := range app.Spec.Variables { + env := corev1.EnvVar{ + // Spin Variables only allow lowercase ascii characters, `_`, and numbers. + // this means that we can do a relatively simple conversion here and in + // the future should implement stronger validation in the webhook/crd definition. + Name: fmt.Sprintf("SPIN_VARIABLE_%s", strings.ToUpper(variable.Name)), + Value: variable.Value, + ValueFrom: variable.ValueFrom, + } + envs[idx] = env + } + + return envs +} + +func mapList[V, Y any](input []V, mapper func(V) Y) []Y { + result := make([]Y, len(input)) + for idx, value := range input { + result[idx] = mapper(value) + } + return result +} + +func SpinHealthCheckToCoreProbe(probe *spinv1.HealthProbe) (*corev1.Probe, error) { + if probe == nil { + return nil, nil + } + + if probe.HTTPGet == nil { + // When the probe is specified, but httpGet is nil, we probably updated the CRD + // without updating the code. This error is a little janky, but shouldn't ever be seen by + // an end user. + return nil, errors.New("probe exists but with unknown configuration, expected httpGet") + } + + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: probe.HTTPGet.Path, + Port: intstr.FromInt(spinapp.DefaultHTTPPort), + HTTPHeaders: mapList(probe.HTTPGet.HTTPHeaders, func(h spinv1.HTTPHealthProbeHeader) corev1.HTTPHeader { + return corev1.HTTPHeader{ + Name: h.Name, + Value: h.Value, + } + }), + }, + }, + InitialDelaySeconds: probe.InitialDelaySeconds, + TimeoutSeconds: probe.TimeoutSeconds, + PeriodSeconds: probe.PeriodSeconds, + SuccessThreshold: probe.SuccessThreshold, + FailureThreshold: probe.FailureThreshold, + }, nil +} + +func ConstructPodHealthChecks(app *spinv1.SpinApp) (readiness *corev1.Probe, liveness *corev1.Probe, err error) { + if app.Spec.Checks.Readiness == nil && app.Spec.Checks.Liveness == nil { + return nil, nil, nil + } + + readiness, err = SpinHealthCheckToCoreProbe(app.Spec.Checks.Readiness) + if err != nil { + return nil, nil, fmt.Errorf("failed to construct readiness probe: %w", err) + } + + liveness, err = SpinHealthCheckToCoreProbe(app.Spec.Checks.Liveness) + if err != nil { + return nil, nil, fmt.Errorf("failed to construct liveness probe: %w", err) + } + + return readiness, liveness, nil +} diff --git a/internal/controller/deployment_test.go b/internal/controller/deployment_test.go new file mode 100644 index 00000000..f9f6a040 --- /dev/null +++ b/internal/controller/deployment_test.go @@ -0,0 +1,243 @@ +package controller + +import ( + "context" + "testing" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/pkg/spinapp" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +func minimalSpinApp() *spinv1.SpinApp { + return &spinv1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: spinv1.SpinAppSpec{ + Executor: "containerd-shim-spin", + Image: "fakereg.dev/noapp:latest", + Replicas: 1, + }, + } +} + +func TestConstructRuntimeConfigSecretMount_Contract(t *testing.T) { + t.Parallel() + + volume, mount := constructRuntimeConfigSecretMount(context.Background(), "my-secret-v1") + // We currently expect runtime config to be optional. + // TODO: evaluate whether we should require this - silently not loading config + // feels subpar. + require.True(t, *volume.VolumeSource.Secret.Optional) + + // Require the volume to be spin- prefixed to avoid collisions with user volumes. + require.Contains(t, volume.Name, "spin-") + + // Require the volume mount to be spin- prefixed to avoid collisions with user volumes. + require.Contains(t, mount.Name, "spin-") +} + +func TestConstructVolumeMountsForApp_Contract(t *testing.T) { + t.Parallel() + + // There should be an error when trying to load runtime-config from multiple + // places. + app := minimalSpinApp() + app.Spec.RuntimeConfig.LoadFromSecret = "a-secret" + _, _, err := ConstructVolumeMountsForApp(context.Background(), app, "a-generated-secret") + require.Error(t, err) + require.ErrorContains(t, err, "cannot specify both a user-provided runtime secret and a generated one") + + // No runtime secret at all is ok + app = minimalSpinApp() + app.Spec.RuntimeConfig.LoadFromSecret = "" + volumes, mounts, err := ConstructVolumeMountsForApp(context.Background(), app, "") + require.NoError(t, err) + require.Empty(t, volumes) + require.Empty(t, mounts) + + // User provided runtime secret is ok + app = minimalSpinApp() + app.Spec.RuntimeConfig.LoadFromSecret = "foo-secret-v1" + volumes, mounts, err = ConstructVolumeMountsForApp(context.Background(), app, "") + require.NoError(t, err) + require.Len(t, volumes, 1) + require.Len(t, mounts, 1) + require.Equal(t, "foo-secret-v1", volumes[0].VolumeSource.Secret.SecretName) + + // Generated runtime secret is ok + app = minimalSpinApp() + app.Spec.RuntimeConfig.LoadFromSecret = "" + volumes, mounts, err = ConstructVolumeMountsForApp(context.Background(), app, "gen-secret") + require.NoError(t, err) + require.Len(t, volumes, 1) + require.Len(t, mounts, 1) + require.Equal(t, "gen-secret", volumes[0].VolumeSource.Secret.SecretName) +} + +func TestConstructEnvForApp(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + varName string + expectedEnvName string + + value string + valueFrom *corev1.EnvVarSource + }{ + { + name: "simple_secret_with_static_value", + varName: "simple_secret", + expectedEnvName: "SPIN_VARIABLE_SIMPLE_SECRET", + value: "f00", + }, + { + name: "simple_secret_with_numb3rs_and_static_value", + varName: "simple_secret_with_numb3rs", + expectedEnvName: "SPIN_VARIABLE_SIMPLE_SECRET_WITH_NUMB3RS", + value: "f00", + }, + { + name: "simple_secret_with_secret_value", + varName: "simple_secret", + expectedEnvName: "SPIN_VARIABLE_SIMPLE_SECRET", + valueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret", + }, + }, + }, + }, + { + name: "pod_attribute_value", + varName: "pod_namespace", + expectedEnvName: "SPIN_VARIABLE_POD_NAMESPACE", + valueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + app := minimalSpinApp() + app.Spec.Variables = []spinv1.SpinVar{ + { + Name: test.varName, + Value: test.value, + ValueFrom: test.valueFrom, + }, + } + + envs := ConstructEnvForApp(context.Background(), app) + + require.Equal(t, test.expectedEnvName, envs[0].Name) + require.Equal(t, test.value, envs[0].Value) + require.Equal(t, test.valueFrom, envs[0].ValueFrom) + }) + } +} + +func TestSpinHealthCheckToCoreProbe(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + probe *spinv1.HealthProbe + expectedProbe *corev1.Probe + expectedErr string + }{ + { + name: "no_probe", + probe: nil, + expectedProbe: nil, + }, + { + name: "probe_missing_httpGet_spec", + probe: &spinv1.HealthProbe{}, + expectedProbe: nil, + expectedErr: "probe exists but with unknown configuration", + }, + { + name: "probe_full", + probe: &spinv1.HealthProbe{ + HTTPGet: &spinv1.HTTPHealthProbe{ + Path: "/var", + HTTPHeaders: []spinv1.HTTPHealthProbeHeader{ + { + Name: "header", + Value: "value", + }, + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 2, + PeriodSeconds: 3, + SuccessThreshold: 4, + FailureThreshold: 5, + }, + expectedProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/var", + Port: intstr.FromInt(80), + HTTPHeaders: []corev1.HTTPHeader{ + { + Name: "header", + Value: "value", + }, + }, + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 2, + PeriodSeconds: 3, + SuccessThreshold: 4, + FailureThreshold: 5, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := SpinHealthCheckToCoreProbe(test.probe) + if test.expectedErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, test.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, test.expectedProbe, result) + }) + } +} + +func TestDeploymentLabel(t *testing.T) { + scheme := registerAndGetScheme() + app := minimalSpinApp() + deployment, err := constructDeployment(context.Background(), app, scheme) + + require.Nil(t, err) + require.NotNil(t, deployment.ObjectMeta.Labels) + require.Equal(t, deployment.ObjectMeta.Labels[spinapp.NameLabelKey], app.Name) +} + +func registerAndGetScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(spinv1.AddToScheme(scheme)) + + return scheme +} diff --git a/internal/controller/service.go b/internal/controller/service.go new file mode 100644 index 00000000..9e898a64 --- /dev/null +++ b/internal/controller/service.go @@ -0,0 +1,59 @@ +package controller + +import ( + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/pkg/spinapp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// constructService builds a corev1.Service based on the configuration of a SpinApp. +func constructService(app *spinv1.SpinApp) *corev1.Service { + annotations := app.Spec.ServiceAnnotations + if annotations == nil { + annotations = map[string]string{} + } + + labels := constructAppLabels(app) + + statusKey, statusValue := spinapp.ConstructStatusReadyLabel(app.Name) + selector := map[string]string{statusKey: statusValue} + + svc := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: app.Name, + Namespace: app.Namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.IntOrString{ + Type: intstr.String, + StrVal: spinapp.HTTPPortName, + }, + Port: spinapp.DefaultHTTPPort, + }, + }, + Selector: selector, + }, + } + + return svc +} + +// constructAppLabels returns the labels to add to deployment/service +// objects for the given SpinApp +func constructAppLabels(app *spinv1.SpinApp) map[string]string { + return map[string]string{ + spinapp.NameLabelKey: app.Name, + } +} diff --git a/internal/controller/service_test.go b/internal/controller/service_test.go new file mode 100644 index 00000000..7561a681 --- /dev/null +++ b/internal/controller/service_test.go @@ -0,0 +1,31 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConstructService(t *testing.T) { + t.Parallel() + + app := minimalSpinApp() + svc := constructService(app) + + // Omitting these is likely to result in client breakage, thus require a test + // change. + require.Equal(t, "Service", svc.TypeMeta.Kind) + require.Equal(t, "v1", svc.TypeMeta.APIVersion) + + // We expect that the service object has the app name and nothing else. + require.Equal(t, map[string]string{"core.spinoperator.dev/app-name": "my-app"}, svc.ObjectMeta.Labels) + // We expect that the service selector has the app status and nothing else. + require.Equal(t, map[string]string{"core.spinoperator.dev/app.my-app.status": "ready"}, svc.Spec.Selector) + + // We expect that the HTTP Port is part of the service. There's currently no + // non-http implementations of a Spin trigger in Kubernetes, thus nothing that + // would change this. + require.Len(t, svc.Spec.Ports, 1) + require.Equal(t, int32(80), svc.Spec.Ports[0].Port) + require.Equal(t, "http-app", svc.Spec.Ports[0].TargetPort.StrVal) +} diff --git a/internal/controller/spinapp_controller.go b/internal/controller/spinapp_controller.go new file mode 100644 index 00000000..b9297a88 --- /dev/null +++ b/internal/controller/spinapp_controller.go @@ -0,0 +1,401 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/constants" + "github.com/spinkube/spin-operator/internal/logging" + "github.com/spinkube/spin-operator/pkg/spinapp" +) + +const ( + // HTTPAppPortName is the name of the port serving an app + HTTPAppPortName = "http-app" + + // SpinOperatorFinalizer is the finalizer used by the spin operator + SpinOperatorFinalizer = "core.spinoperator.dev/finalizer" + + // FieldManger is used to declare that the spin operator owns specific fields on child resources + FieldManager = "spin-operator" +) + +// SpinAppReconciler reconciles a SpinApp object +type SpinAppReconciler struct { + Client client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=core.spinoperator.dev,resources=spinapps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.spinoperator.dev,resources=spinapps/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get +//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete + +// SetupWithManager sets up the controller with the Manager. +func (r *SpinAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&spinv1.SpinApp{}). + // Owns allows watching dependency resources for any changes + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Complete(r) +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile +func (r *SpinAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logging.FromContext(ctx) + log.Debug("Reconciling SpinApp") + + // Check if the SpinApp exists + var spinApp spinv1.SpinApp + if err := r.Client.Get(ctx, req.NamespacedName, &spinApp); err != nil { + // TODO: This error logging is noisy + log.Error(err, "Unable to fetch SpinApp") + // we'll ignore not-found errors, since they can't be fixed by an immediate + // requeue (we'll need to wait for a new notification), and we can get them + // on deleted requests. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Update the status of the SpinApp + err := r.updateStatus(ctx, &spinApp) + if err != nil { + return ctrl.Result{}, err + } + + // Spin app has been requested for deletion, child resources will + // automatically be deleted. + if !spinApp.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + // Reconcile the child resources + switch spinApp.Spec.Executor { + case constants.CyclotronExecutor: + // Cyclotron does not use a deployment but it may exist if we were previously using a different executor + err := r.deleteDeployment(ctx, &spinApp) + if client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, err + } + + err = r.reconcileService(ctx, &spinApp) + if err != nil { + return ctrl.Result{}, err + } + case constants.ContainerDShimSpinExecutor: + err := r.reconcileDeployment(ctx, &spinApp) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcileService(ctx, &spinApp) + if err != nil { + return ctrl.Result{}, err + } + default: + return ctrl.Result{}, fmt.Errorf("unknown executor %s", spinApp.Spec.Executor) + } + + return ctrl.Result{}, nil +} + +// updateStatus updates the status of a SpinApp. +func (r *SpinAppReconciler) updateStatus(ctx context.Context, app *spinv1.SpinApp) error { + log := logging.FromContext(ctx) + + // TODO: Just ignoring the cyclotron case for now + if app.Spec.Executor == constants.CyclotronExecutor { + return nil + } + + // Set the active scheduler + app.Status.ActiveScheduler = app.Spec.Executor + + deployment, err := r.findDeploymentForApp(ctx, app) + if client.IgnoreNotFound(err) != nil { + log.Error(err, "Unable to find deployment for app") + return err + } + + if apierrors.IsNotFound(err) { + // Deployment doesn't exist yet so set conditions as unknown + meta.SetStatusCondition( + &app.Status.Conditions, + metav1.Condition{ + Type: "Available", + Status: metav1.ConditionUnknown, + Reason: "DeploymentNotFound", + Message: "Deployment not found", + }) + meta.SetStatusCondition( + &app.Status.Conditions, + metav1.Condition{ + Type: "Progressing", + Status: metav1.ConditionUnknown, + Reason: "DeploymentNotFound", + Message: "Deployment not found", + }) + app.Status.ReadyReplicas = 0 + } else { + deploymentConditions := deployment.Status.Conditions + for _, dc := range deploymentConditions { + if dc.Type == appsv1.DeploymentAvailable { + meta.SetStatusCondition( + &app.Status.Conditions, + metav1.Condition{ + Type: "Available", + Status: metav1.ConditionStatus(dc.Status), + Reason: dc.Reason, + Message: dc.Message, + }) + } + if dc.Type == appsv1.DeploymentProgressing { + meta.SetStatusCondition( + &app.Status.Conditions, + metav1.Condition{ + Type: "Progressing", + Status: metav1.ConditionStatus(dc.Status), + Reason: dc.Reason, + Message: dc.Message, + }) + } + } + app.Status.ReadyReplicas = deployment.Status.ReadyReplicas + } + + if err := r.Client.Status().Update(ctx, app); err != nil { + log.Error(err, "Unable to update status") + } + + // Re-fetch app to avoid "object has been modified" errors + if err := r.Client.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, app); err != nil { + log.Error(err, "Unable to re-fetch app") + return err + } + + return nil +} + +// reconcileDeployment creates a deployment if one does not exist and reconciles it if it does. +func (r *SpinAppReconciler) reconcileDeployment(ctx context.Context, app *spinv1.SpinApp) error { + log := logging.FromContext(ctx).WithValues("deployment", app.Name) + + desiredDeployment, err := constructDeployment(ctx, app, r.Scheme) + if err != nil { + log.Error(err, "Unable to construct Deployment") + return err + } + + log.Debug("Reconciling Deployment") + + // We want to use server-side apply https://kubernetes.io/docs/reference/using-api/server-side-apply + patchMethod := client.Apply + patchOptions := &client.PatchOptions{ + Force: ptr(true), // Force b/c any fields we are setting need to be owned by the spin-operator + FieldManager: FieldManager, + } + + // Note that we reconcile even if the deployment is in a good state. We rely on controller-runtime to rate limit us. + if err := r.Client.Patch(ctx, desiredDeployment, patchMethod, patchOptions); err != nil { + log.Error(err, "Unable to reconcile Deployment") + return err + } + + return nil +} + +// reconcileService creates a service if one does not exist and updates it if it does. +func (r *SpinAppReconciler) reconcileService(ctx context.Context, app *spinv1.SpinApp) error { + log := logging.FromContext(ctx).WithValues("service", app.Name) + + desiredService := constructService(app) + if err := ctrl.SetControllerReference(app, desiredService, r.Scheme); err != nil { + log.Error(err, "Unable to construct Service") + return err + } + + log.Debug("Reconciling Service") + + // We want to use server-side apply https://kubernetes.io/docs/reference/using-api/server-side-apply + patchMethod := client.Apply + patchOptions := &client.PatchOptions{ + Force: ptr(true), // Force b/c any fields we are setting need to be owned by the spin-operator + FieldManager: FieldManager, + } + // Note that we reconcile even if the service is in a good state. We rely on controller-runtime to rate limit us. + if err := r.Client.Patch(ctx, desiredService, patchMethod, patchOptions); err != nil { + log.Error(err, "Unable to reconcile Service") + return err + } + + return nil +} + +// deleteDeployment deletes the deployment for a SpinApp. +func (r *SpinAppReconciler) deleteDeployment(ctx context.Context, app *spinv1.SpinApp) error { + deployment, err := r.findDeploymentForApp(ctx, app) + if err != nil { + return err + } + + err = r.Client.Delete(ctx, deployment) + if err != nil { + return err + } + + return nil +} + +// constructDeployment builds an appsv1.Deployment based on the configuration of a SpinApp. +func constructDeployment(ctx context.Context, app *spinv1.SpinApp, scheme *runtime.Scheme) (*appsv1.Deployment, error) { + spinRuntimeClassName := "wasmtime-spin-v2" + + // TODO: Once we land admission webhooks write some validation to make + // replicas and enableAutoscaling mutually exclusive. + var replicas *int32 + if app.Spec.EnableAutoscaling { + replicas = nil + } else { + replicas = ptr(app.Spec.Replicas) + } + + volumes, volumeMounts, err := ConstructVolumeMountsForApp(ctx, app, "") + if err != nil { + return nil, err + } + + annotations := app.Spec.DeploymentAnnotations + if annotations == nil { + annotations = map[string]string{} + } + templateAnnotations := app.Spec.PodAnnotations + if templateAnnotations == nil { + templateAnnotations = map[string]string{} + } + + statusKey, statusValue := spinapp.ConstructStatusReadyLabel(app.Name) + readyLabels := map[string]string{ + spinapp.NameLabelKey: app.Name, + statusKey: statusValue, + } + + // TODO: Once we land admission webhooks write some validation for this e.g. + // don't allow setting memory limit with cyclotron runtime. + resources := corev1.ResourceRequirements{ + Limits: app.Spec.Resources.Limits, + Requests: app.Spec.Resources.Requests, + } + + env := ConstructEnvForApp(ctx, app) + + readinessProbe, livenessProbe, err := ConstructPodHealthChecks(app) + if err != nil { + return nil, err + } + + labels := constructAppLabels(app) + + dep := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: app.Name, + Namespace: app.Namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: readyLabels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: readyLabels, + Annotations: templateAnnotations, + }, + Spec: corev1.PodSpec{ + RuntimeClassName: &spinRuntimeClassName, + Containers: []corev1.Container{ + { + Name: app.Name, + Image: app.Spec.Image, + Command: []string{"/"}, + Ports: []corev1.ContainerPort{{ + Name: spinapp.HTTPPortName, + ContainerPort: spinapp.DefaultHTTPPort, + }}, + Env: env, + VolumeMounts: volumeMounts, + Resources: resources, + LivenessProbe: livenessProbe, + ReadinessProbe: readinessProbe, + }, + }, + ImagePullSecrets: app.Spec.ImagePullSecrets, + Volumes: volumes, + }, + }, + }, + } + + // Set the controller reference, specifying that these resources are controlled by the SpinApp + // being reconciled + // TODO: Move this out of the "constructor" or otherwise abstract the setter + // to not depend on controller-runtime api for testing "pure" data code. + if scheme != nil { + if err := ctrl.SetControllerReference(app, dep, scheme); err != nil { + return nil, err + } + } + + return dep, nil +} + +// findDeploymentForApp finds the deployment for a SpinApp. +func (r *SpinAppReconciler) findDeploymentForApp(ctx context.Context, app *spinv1.SpinApp) (*appsv1.Deployment, error) { + var deployment appsv1.Deployment + err := r.Client.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, &deployment) + if err != nil { + return nil, err + } + return &deployment, nil +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/internal/controller/spinapp_controller_test.go b/internal/controller/spinapp_controller_test.go new file mode 100644 index 00000000..1c3d30e3 --- /dev/null +++ b/internal/controller/spinapp_controller_test.go @@ -0,0 +1,107 @@ +package controller + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + "time" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +type envTestState struct { + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +} + +// SetupEnvTest will start a fake kubernetes and client for use when testing +// reconciliation loops that require a kubernetes api. +func SetupEnvTest(t *testing.T) *envTestState { + t.Helper() + + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without calling the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + cfg, err := testEnv.Start() + if err != nil { + t.Skipf("envtest unavailable: %v", err) + } + + require.NoError(t, err) + require.NotNil(t, cfg) + + err = spinv1.AddToScheme(scheme.Scheme) + require.NoError(t, err) + + k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) + require.NoError(t, err) + require.NotNil(t, k8sClient) + + t.Cleanup(func() { + err := testEnv.Stop() + require.NoError(t, err) + }) + + return &envTestState{ + cfg: cfg, + k8sClient: k8sClient, + testEnv: testEnv, + } +} + +func TestReconcile_Integration_StartupShutdown(t *testing.T) { + t.Parallel() + + envTest := SetupEnvTest(t) + + ctrlr := &SpinAppReconciler{ + Client: envTest.k8sClient, + Scheme: scheme.Scheme, + } + + mgr, err := ctrl.NewManager(envTest.cfg, manager.Options{ + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + require.NoError(t, err) + + require.NoError(t, ctrlr.SetupWithManager(mgr)) + + ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFunc() + require.NoError(t, mgr.Start(ctx)) +} + +func TestConstructDeployment_MinimalApp(t *testing.T) { + t.Parallel() + + app := minimalSpinApp() + + deployment, err := constructDeployment(context.Background(), app, nil) + require.NoError(t, err) + require.NotNil(t, deployment) + + require.Equal(t, ptr(int32(1)), deployment.Spec.Replicas) + require.Len(t, deployment.Spec.Template.Spec.Containers, 1) + require.Equal(t, app.Spec.Image, deployment.Spec.Template.Spec.Containers[0].Image) +} diff --git a/internal/controller/spinappexecutor_controller.go b/internal/controller/spinappexecutor_controller.go new file mode 100644 index 00000000..f5501e61 --- /dev/null +++ b/internal/controller/spinappexecutor_controller.go @@ -0,0 +1,121 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "errors" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/logging" +) + +// SpinAppExecutorReconciler reconciles a SpinAppExecutor object +type SpinAppExecutorReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=core.spinoperator.dev,resources=spinappexecutors,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.spinoperator.dev,resources=spinappexecutors/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.spinoperator.dev,resources=spinappexecutors/finalizers,verbs=update + +// SetupWithManager sets up the controller with the Manager. +func (r *SpinAppExecutorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&spinv1.SpinAppExecutor{}). + Complete(r) +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile +func (r *SpinAppExecutorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logging.FromContext(ctx) + log.Debug("Reconciling SpinAppExecutor") + + // Check if the SpinAppExecutor exists + var executor spinv1.SpinAppExecutor + if err := r.Client.Get(ctx, req.NamespacedName, &executor); err != nil { + log.Error(err, "Unable to fetch SpinAppExecutor") + // we'll ignore not-found errors, since they can't be fixed by an immediate + // requeue (we'll need to wait for a new notification), and we can get them + // on deleted requests. + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // SpinAppExecutor has been requested for deletion + if !executor.DeletionTimestamp.IsZero() { + err := r.handleDeletion(ctx, &executor) + if err != nil { + return ctrl.Result{}, err + } + + err = r.removeFinalizer(ctx, &executor) + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Make sure the finalizer is present + err := r.ensureFinalizer(ctx, &executor) + return ctrl.Result{}, client.IgnoreNotFound(err) +} + +// handleDeletion makes sure no SpinApps are dependent on the SpinAppExecutor +// before allowing it to be deleted. +func (r *SpinAppExecutorReconciler) handleDeletion(ctx context.Context, executor *spinv1.SpinAppExecutor) error { + var spinApps spinv1.SpinAppList + if err := r.Client.List(ctx, &spinApps, client.MatchingFields{"spec.runtime": executor.Name}); err != nil { + // TODO: Log this + // TODO: Emit k8s event + return err + } + + if len(spinApps.Items) > 0 { + return errors.New("cannot delete SpinAppExecutor with dependent SpinApps") + } + + return nil +} + +// removeFinalizer removes the finalizer from a SpinAppExecutor. +func (r *SpinAppExecutorReconciler) removeFinalizer(ctx context.Context, executor *spinv1.SpinAppExecutor) error { + if controllerutil.ContainsFinalizer(executor, SpinOperatorFinalizer) { + controllerutil.RemoveFinalizer(executor, SpinOperatorFinalizer) + if err := r.Client.Update(ctx, executor); err != nil { + return err + } + } + return nil +} + +// ensureFinalizer ensures the finalizer is present on a SpinAppExecutor. +func (r *SpinAppExecutorReconciler) ensureFinalizer(ctx context.Context, executor *spinv1.SpinAppExecutor) error { + if !controllerutil.ContainsFinalizer(executor, SpinOperatorFinalizer) { + controllerutil.AddFinalizer(executor, SpinOperatorFinalizer) + if err := r.Client.Update(ctx, executor); err != nil { + return err + } + } + return nil +} diff --git a/internal/controller/spinappexecutor_controller_test.go b/internal/controller/spinappexecutor_controller_test.go new file mode 100644 index 00000000..c4e3aea3 --- /dev/null +++ b/internal/controller/spinappexecutor_controller_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +// import ( +// "fmt" +// "path/filepath" +// "runtime" +// "testing" + +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" + +// "k8s.io/client-go/kubernetes/scheme" +// "k8s.io/client-go/rest" +// "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/envtest" +// logf "sigs.k8s.io/controller-runtime/pkg/log" +// "sigs.k8s.io/controller-runtime/pkg/log/zap" + +// spinv1 "github.com/spinkube/spin-operator/api/v1" +// //+kubebuilder:scaffold:imports +// ) + +// // These tests use Ginkgo (BDD-style Go testing framework). Refer to +// // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +// var cfg *rest.Config +// var k8sClient client.Client +// var testEnv *envtest.Environment + +// func TestControllers(t *testing.T) { +// RegisterFailHandler(Fail) + +// RunSpecs(t, "Controller Suite") +// } + +// var _ = BeforeSuite(func() { +// logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + +// By("bootstrapping test environment") +// testEnv = &envtest.Environment{ +// CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, +// ErrorIfCRDPathMissing: true, + +// // The BinaryAssetsDirectory is only required if you want to run the tests directly +// // without call the makefile target test. If not informed it will look for the +// // default path defined in controller-runtime which is /usr/local/kubebuilder/. +// // Note that you must have the required binaries setup under the bin directory to perform +// // the tests directly. When we run make test it will be setup and used automatically. +// BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", +// fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), +// } + +// var err error +// // cfg is defined in this file globally. +// cfg, err = testEnv.Start() +// Expect(err).NotTo(HaveOccurred()) +// Expect(cfg).NotTo(BeNil()) + +// err = spinv1.AddToScheme(scheme.Scheme) +// Expect(err).NotTo(HaveOccurred()) + +// //+kubebuilder:scaffold:scheme + +// k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) +// Expect(err).NotTo(HaveOccurred()) +// Expect(k8sClient).NotTo(BeNil()) + +// }) + +// var _ = AfterSuite(func() { +// By("tearing down the test environment") +// err := testEnv.Stop() +// Expect(err).NotTo(HaveOccurred()) +// }) diff --git a/internal/logging/logr_logger.go b/internal/logging/logr_logger.go new file mode 100644 index 00000000..ff7587fa --- /dev/null +++ b/internal/logging/logr_logger.go @@ -0,0 +1,85 @@ +// Package logging provides the operators's recommended logging interface. +// +// The logging interface avoids the complexity of levels and provides a simpler +// api that makes it harder to introduce unnecesasry ambiguity to logs (or +// ascribing value to arbitrary magic numbers). +// +// An Error logging helper exists primarily to facilitate including a stack trace +// when the backing provider supports it. +package logging + +import ( + "context" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// A Logger logs messages. Messages may be supplemented by structured data. +type Logger interface { + // Info logs a message with optional structured data. Structured data must + // be supplied as an array that alternates between string keys and values of + // an arbitrary type. Use Info for messages that users are + // very likely to be concerned with. + Info(msg string, keysAndValues ...any) + + // Error logs a message with optional structured data. Structured data must + // be supplied as an array that alternates between string keys and values of + // an arbitrary type. Use Error when you want to enrich a message with as much + // information as a logging provider can. + Error(err error, msg string, keysAndValues ...any) + + // Debug logs a message with optional structured data. Structured data must + // be supplied as an array that alternates between string keys and values of + // an arbitrary type. Use Debug for messages that operators or + // developers may be concerned with when debugging the operator or spin. + Debug(msg string, keysAndValues ...any) + + // WithValues returns a Logger that will include the supplied structured + // data with any subsequent messages it logs. Structured data must + // be supplied as an array that alternates between string keys and values of + // an arbitrary type. + WithValues(keysAndValues ...any) Logger +} + +// NewNopLogger returns a Logger that does nothing. +func NewNopLogger() Logger { return nopLogger{} } + +type nopLogger struct{} + +func (l nopLogger) Info(_ string, _ ...any) {} +func (l nopLogger) Debug(_ string, _ ...any) {} +func (l nopLogger) Error(_ error, _ string, _ ...any) {} +func (l nopLogger) WithValues(_ ...any) Logger { return nopLogger{} } + +// NewLogrLogger returns a Logger that is satisfied by the supplied logr.Logger, +// which may be satisfied in turn by various logging implementations. +// Debug messages are logged at V(1) - following the reccomendation of +// controller-runtime. +func NewLogrLogger(l logr.Logger) Logger { + return logrLogger{log: l} +} + +type logrLogger struct { + log logr.Logger +} + +func (l logrLogger) Info(msg string, keysAndValues ...any) { + l.log.Info(msg, keysAndValues...) +} + +func (l logrLogger) Error(err error, msg string, keysAndValues ...any) { + l.log.Error(err, msg, keysAndValues...) +} + +func (l logrLogger) Debug(msg string, keysAndValues ...any) { + l.log.V(1).Info(msg, keysAndValues...) +} + +func (l logrLogger) WithValues(keysAndValues ...any) Logger { + return logrLogger{log: l.log.WithValues(keysAndValues...)} +} + +func FromContext(ctx context.Context) Logger { + return logrLogger{log: log.FromContext(ctx)} +} diff --git a/internal/webhook/admission.go b/internal/webhook/admission.go new file mode 100644 index 00000000..e5ee1536 --- /dev/null +++ b/internal/webhook/admission.go @@ -0,0 +1,22 @@ +package webhook + +import ( + spinv1 "github.com/spinkube/spin-operator/api/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +func SetupSpinAppWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&spinv1.SpinApp{}). + WithDefaulter(&SpinAppDefaulter{Client: mgr.GetClient()}). + WithValidator(&SpinAppValidator{Client: mgr.GetClient()}). + Complete() +} + +func SetupSpinAppExecutorWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&spinv1.SpinAppExecutor{}). + WithDefaulter(&SpinAppExecutorDefaulter{Client: mgr.GetClient()}). + WithValidator(&SpinAppExecutorValidator{Client: mgr.GetClient()}). + Complete() +} diff --git a/internal/webhook/admission_test.go b/internal/webhook/admission_test.go new file mode 100644 index 00000000..4a1270dd --- /dev/null +++ b/internal/webhook/admission_test.go @@ -0,0 +1,260 @@ +package webhook + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "runtime" + "testing" + "time" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/constants" + "github.com/stretchr/testify/require" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +type envTestState struct { + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +} + +// setupEnvTest will start a fake kubernetes and client for use when testing +// webhooks that require a kubernetes api. +func setupEnvTest(t *testing.T) *envTestState { + t.Helper() + + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without calling the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + cfg, err := testEnv.Start() + if err != nil { + t.Skipf("envtest unavailable: %v", err) + } + + require.NoError(t, err) + require.NotNil(t, cfg) + + err = spinv1.AddToScheme(scheme.Scheme) + require.NoError(t, err) + + err = admissionv1.AddToScheme(scheme.Scheme) + require.NoError(t, err) + + k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) + require.NoError(t, err) + require.NotNil(t, k8sClient) + + return &envTestState{ + cfg: cfg, + k8sClient: k8sClient, + testEnv: testEnv, + } +} + +func startWebhookServer(t *testing.T, envtest *envTestState) { + t.Helper() + + // start webhook server using Manager + webhookInstallOptions := &envtest.testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(envtest.cfg, ctrl.Options{ + Scheme: scheme.Scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + require.NoError(t, err) + + err = SetupSpinAppWebhookWithManager(mgr) + require.NoError(t, err) + + err = SetupSpinAppExecutorWebhookWithManager(mgr) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + err = mgr.Start(ctx) + require.NoError(t, err) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + require.Eventually(t, func() bool { + // nolint:gosec + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return false + } + err = conn.Close() + return err == nil + }, 10*time.Second, 2*time.Second) + + t.Cleanup(func() { + err := envtest.testEnv.Stop() + require.NoError(t, err) + }) + + // As per https://github.com/kubernetes-sigs/controller-runtime/issues/1571 to avoid leaking kube-apiserver and etcd + t.Cleanup(func() { + cancel() + }) +} + +func TestCreateSpinAppWithNoExecutor(t *testing.T) { + t.Parallel() + + envtest := setupEnvTest(t) + startWebhookServer(t, envtest) + + err := envtest.k8sClient.Create(context.Background(), &spinv1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spinapp", + Namespace: "default", + }, + Spec: spinv1.SpinAppSpec{ + Image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0", + Replicas: 2, + }, + }) + require.EqualError(t, err, "admission webhook \"vspinapp.kb.io\" denied the request: SpinApp.core.spinoperator.dev \"spinapp\" is invalid:"+ + " spec.executor: Invalid value: \"\": executor must be set, likely no default executor was set because you have no executors installed") +} + +func TestCreateSpinAppWithSingleExecutor(t *testing.T) { + t.Parallel() + + envtest := setupEnvTest(t) + startWebhookServer(t, envtest) + + err := envtest.k8sClient.Create(context.Background(), &spinv1.SpinAppExecutor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cyclotron", + Namespace: "default", + }, + }) + require.NoError(t, err) + + err = envtest.k8sClient.Create(context.Background(), &spinv1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spinapp", + Namespace: "default", + }, + Spec: spinv1.SpinAppSpec{ + Image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0", + Replicas: 2, + }, + }) + require.NoError(t, err) + + spinapp := &spinv1.SpinApp{} + err = envtest.k8sClient.Get(context.Background(), client.ObjectKey{ + Name: "spinapp", + Namespace: "default", + }, spinapp) + require.NoError(t, err) + require.Equal(t, constants.CyclotronExecutor, spinapp.Spec.Executor) + require.Equal(t, int32(2), spinapp.Spec.Replicas) +} + +func TestCreateSpinAppWithMultipleExecutors(t *testing.T) { + t.Parallel() + + envtest := setupEnvTest(t) + startWebhookServer(t, envtest) + + err := envtest.k8sClient.Create(context.Background(), &spinv1.SpinAppExecutor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "containerd-shim-spin", + Namespace: "default", + }, + }) + require.NoError(t, err) + + err = envtest.k8sClient.Create(context.Background(), &spinv1.SpinAppExecutor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cyclotron", + Namespace: "default", + }, + }) + require.NoError(t, err) + + err = envtest.k8sClient.Create(context.Background(), &spinv1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spinapp", + Namespace: "default", + }, + Spec: spinv1.SpinAppSpec{ + Image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0", + Replicas: 2, + }, + }) + require.NoError(t, err) + + spinapp := &spinv1.SpinApp{} + err = envtest.k8sClient.Get(context.Background(), client.ObjectKey{ + Name: "spinapp", + Namespace: "default", + }, spinapp) + require.NoError(t, err) + // Correct based on alphabetical order + require.Equal(t, constants.ContainerDShimSpinExecutor, spinapp.Spec.Executor) + require.Equal(t, int32(2), spinapp.Spec.Replicas) +} + +func TestCreateInvalidSpinApp(t *testing.T) { + t.Parallel() + + envtest := setupEnvTest(t) + startWebhookServer(t, envtest) + + err := envtest.k8sClient.Create(context.Background(), &spinv1.SpinAppExecutor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "containerd-shim-spin", + Namespace: "default", + }, + }) + require.NoError(t, err) + + err = envtest.k8sClient.Create(context.Background(), &spinv1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spinapp", + Namespace: "default", + }, + Spec: spinv1.SpinAppSpec{ + Image: "ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0", + Replicas: -1, + }, + }) + require.Error(t, err) +} diff --git a/internal/webhook/spinapp_defaulting.go b/internal/webhook/spinapp_defaulting.go new file mode 100644 index 00000000..5c77f9c3 --- /dev/null +++ b/internal/webhook/spinapp_defaulting.go @@ -0,0 +1,69 @@ +package webhook + +import ( + "context" + "strings" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/logging" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// nolint:lll +//+kubebuilder:webhook:path=/mutate-core-spinoperator-dev-v1-spinapp,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.spinoperator.dev,resources=spinapps,verbs=create;update,versions=v1,name=mspinapp.kb.io,admissionReviewVersions=v1 + +// SpinAppDefaulter mutates SpinApps +type SpinAppDefaulter struct { + Client client.Client +} + +// Default implements webhook.Defaulter +func (d *SpinAppDefaulter) Default(ctx context.Context, obj runtime.Object) error { + log := logging.FromContext(ctx) + + spinApp := obj.(*spinv1.SpinApp) + log.Info("default", "name", spinApp.Name) + + if spinApp.Spec.Executor == "" { + executor, err := d.findDefaultExecutor(ctx) + if err != nil { + return err + } + spinApp.Spec.Executor = executor + } + + return nil +} + +// findDefaultExecutor sets the default executor for a SpinApp. +// +// Defaults to whatever executor is available on the cluster. If multiple +// executors are available then the first executor in alphabetical order +// will be chosen. If no executors are available then no default will be set. +func (d *SpinAppDefaulter) findDefaultExecutor(ctx context.Context) (string, error) { + log := logging.FromContext(ctx) + + var executors spinv1.SpinAppExecutorList + if err := d.Client.List(ctx, &executors); err != nil { + log.Error(err, "failed to list SpinAppExecutors") + return "", err + } + + if len(executors.Items) == 0 { + log.Info("no SpinAppExecutors found") + return "", nil + } + + // Return first executor in alphabetical order + chosenExecutor := executors.Items[0] + for _, executor := range executors.Items[1:] { + // For each item after the first see if it is alphabetically before the current chosen executor + if strings.Compare(executor.Name, chosenExecutor.Name) < 0 { + chosenExecutor = executor + } + } + + log.Info("defaulting to executor", "name", chosenExecutor.Name) + return chosenExecutor.Name, nil +} diff --git a/internal/webhook/spinapp_defaulting_test.go b/internal/webhook/spinapp_defaulting_test.go new file mode 100644 index 00000000..57323d32 --- /dev/null +++ b/internal/webhook/spinapp_defaulting_test.go @@ -0,0 +1,26 @@ +package webhook + +import ( + "context" + "testing" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/constants" + "github.com/stretchr/testify/require" +) + +func TestDefaultNothingToSet(t *testing.T) { + t.Parallel() + + defaulter := &SpinAppDefaulter{} + + spinApp := &spinv1.SpinApp{Spec: spinv1.SpinAppSpec{ + Executor: constants.CyclotronExecutor, + Replicas: 1, + }} + + err := defaulter.Default(context.Background(), spinApp) + require.NoError(t, err) + require.Equal(t, constants.CyclotronExecutor, spinApp.Spec.Executor) + require.Equal(t, int32(1), spinApp.Spec.Replicas) +} diff --git a/internal/webhook/spinapp_validating.go b/internal/webhook/spinapp_validating.go new file mode 100644 index 00000000..ac7c7161 --- /dev/null +++ b/internal/webhook/spinapp_validating.go @@ -0,0 +1,125 @@ +package webhook + +import ( + "context" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/constants" + "github.com/spinkube/spin-operator/internal/logging" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// nolint:lll +//+kubebuilder:webhook:path=/validate-core-spinoperator-dev-v1-spinapp,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.spinoperator.dev,resources=spinapps,verbs=create;update,versions=v1,name=vspinapp.kb.io,admissionReviewVersions=v1 + +// SpinAppValidator validates SpinApps +type SpinAppValidator struct { + Client client.Client +} + +// ValidateCreate implements webhook.Validator +func (v *SpinAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + spinApp := obj.(*spinv1.SpinApp) + log.Info("validate create", "name", spinApp.Name) + + return nil, v.validateSpinApp(ctx, spinApp) +} + +// ValidateUpdate implements webhook.Validator +func (v *SpinAppValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + spinApp := newObj.(*spinv1.SpinApp) + log.Info("validate update", "name", spinApp.Name) + + return nil, v.validateSpinApp(ctx, spinApp) +} + +// ValidateDelete implements webhook.Validator +func (v *SpinAppValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + spinApp := obj.(*spinv1.SpinApp) + log.Info("validate delete", "name", spinApp.Name) + + return nil, nil +} + +func (v *SpinAppValidator) validateSpinApp(ctx context.Context, spinApp *spinv1.SpinApp) error { + var allErrs field.ErrorList + if err := validateExecutor(spinApp.Spec, v.executorExists(ctx, spinApp.Namespace)); err != nil { + allErrs = append(allErrs, err) + } + if err := validateReplicas(spinApp.Spec); err != nil { + allErrs = append(allErrs, err) + } + if err := validateAnnotations(spinApp.Spec); err != nil { + allErrs = append(allErrs, err) + } + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "core.spinoperator.dev", Kind: "SpinApp"}, + spinApp.Name, allErrs) +} + +// executorExists returns a function that checks if an executor exists on the cluster. +// +// We assume that the executor must exist in the same namespace as the SpinApp. +func (v *SpinAppValidator) executorExists(ctx context.Context, spinAppNs string) func(string) bool { + return func(name string) bool { + var executor spinv1.SpinAppExecutor + if err := v.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: spinAppNs}, &executor); err != nil { + // TODO: This groups in both not found and client errors. We should ideally separate the two. + return false + } + + return true + } +} + +func validateExecutor(spec spinv1.SpinAppSpec, executorExists func(name string) bool) *field.Error { + if spec.Executor == "" { + return field.Invalid(field.NewPath("spec").Child("executor"), spec.Executor, "executor must be set, likely no default executor was set because you have no executors installed") + } + if !executorExists(spec.Executor) { + return field.Invalid(field.NewPath("spec").Child("executor"), spec.Executor, "executor does not exist on cluster") + } + + return nil +} + +func validateReplicas(spec spinv1.SpinAppSpec) *field.Error { + if spec.EnableAutoscaling && spec.Replicas != 0 { + return field.Invalid(field.NewPath("spec").Child("replicas"), spec.Replicas, "replicas cannot be set when autoscaling is enabled") + } + if !spec.EnableAutoscaling && spec.Replicas < 1 { + return field.Invalid(field.NewPath("spec").Child("replicas"), spec.Replicas, "replicas must be > 0") + } + + return nil +} + +func validateAnnotations(spec spinv1.SpinAppSpec) *field.Error { + if spec.Executor != constants.CyclotronExecutor { + return nil + } + if len(spec.DeploymentAnnotations) != 0 { + return field.Invalid(field.NewPath("spec").Child("deploymentAnnotations"), spec.DeploymentAnnotations, "deploymentAnnotations can't be set when runtime is cyclotron") + } + if len(spec.PodAnnotations) != 0 { + return field.Invalid(field.NewPath("spec").Child("podAnnotations"), spec.PodAnnotations, "podAnnotations can't be set when runtime is cyclotron") + } + + return nil +} diff --git a/internal/webhook/spinapp_validating_test.go b/internal/webhook/spinapp_validating_test.go new file mode 100644 index 00000000..8ebd4c53 --- /dev/null +++ b/internal/webhook/spinapp_validating_test.go @@ -0,0 +1,63 @@ +package webhook + +import ( + "testing" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/constants" + "github.com/stretchr/testify/require" +) + +func TestValidateExecutor(t *testing.T) { + t.Parallel() + + fldErr := validateExecutor(spinv1.SpinAppSpec{}, func(string) bool { return true }) + require.EqualError(t, fldErr, "spec.executor: Invalid value: \"\": executor must be set, likely no default executor was set because you have no executors installed") + + fldErr = validateExecutor(spinv1.SpinAppSpec{Executor: constants.CyclotronExecutor}, func(string) bool { return false }) + require.EqualError(t, fldErr, "spec.executor: Invalid value: \"cyclotron\": executor does not exist on cluster") + + fldErr = validateExecutor(spinv1.SpinAppSpec{Executor: constants.ContainerDShimSpinExecutor}, func(name string) bool { return true }) + require.Nil(t, fldErr) +} + +func TestValidateReplicas(t *testing.T) { + t.Parallel() + + fldErr := validateReplicas(spinv1.SpinAppSpec{}) + require.EqualError(t, fldErr, "spec.replicas: Invalid value: 0: replicas must be > 0") + + fldErr = validateReplicas(spinv1.SpinAppSpec{Replicas: 1}) + require.Nil(t, fldErr) +} + +func TestValidateAnnotations(t *testing.T) { + t.Parallel() + + fldErr := validateAnnotations(spinv1.SpinAppSpec{ + Executor: constants.CyclotronExecutor, + DeploymentAnnotations: map[string]string{"key": "asdf"}, + }) + require.EqualError(t, fldErr, + `spec.deploymentAnnotations: Invalid value: map[string]string{"key":"asdf"}: `+ + `deploymentAnnotations can't be set when runtime is cyclotron`) + + fldErr = validateAnnotations(spinv1.SpinAppSpec{ + Executor: constants.CyclotronExecutor, + PodAnnotations: map[string]string{"key": "asdf"}, + }) + require.EqualError(t, fldErr, + `spec.podAnnotations: Invalid value: map[string]string{"key":"asdf"}: `+ + `podAnnotations can't be set when runtime is cyclotron`) + + fldErr = validateAnnotations(spinv1.SpinAppSpec{ + Executor: constants.ContainerDShimSpinExecutor, + DeploymentAnnotations: map[string]string{"key": "asdf"}, + }) + require.Nil(t, fldErr) + + fldErr = validateAnnotations(spinv1.SpinAppSpec{ + Executor: constants.CyclotronExecutor, + }) + require.Nil(t, fldErr) +} diff --git a/internal/webhook/spinappexecutor_defaulting.go b/internal/webhook/spinappexecutor_defaulting.go new file mode 100644 index 00000000..e147f548 --- /dev/null +++ b/internal/webhook/spinappexecutor_defaulting.go @@ -0,0 +1,28 @@ +package webhook + +import ( + "context" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/logging" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// nolint:lll +//+kubebuilder:webhook:path=/mutate-core-spinoperator-dev-v1-spinappexecutor,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.spinoperator.dev,resources=spinappexecutors,verbs=create;update,versions=v1,name=mspinappexecutor.kb.io,admissionReviewVersions=v1 + +// SpinAppExecutorDefaulter mutates SpinApps +type SpinAppExecutorDefaulter struct { + Client client.Client +} + +// Default implements webhook.Defaulter +func (d *SpinAppExecutorDefaulter) Default(ctx context.Context, obj runtime.Object) error { + log := logging.FromContext(ctx) + + executor := obj.(*spinv1.SpinAppExecutor) + log.Info("default", "name", executor.Name) + + return nil +} diff --git a/internal/webhook/spinappexecutor_defaulting_test.go b/internal/webhook/spinappexecutor_defaulting_test.go new file mode 100644 index 00000000..d903e238 --- /dev/null +++ b/internal/webhook/spinappexecutor_defaulting_test.go @@ -0,0 +1,3 @@ +package webhook + +// Currently the defaulting webhook is a no-op so nothing to test diff --git a/internal/webhook/spinappexecutor_validating.go b/internal/webhook/spinappexecutor_validating.go new file mode 100644 index 00000000..65562daf --- /dev/null +++ b/internal/webhook/spinappexecutor_validating.go @@ -0,0 +1,54 @@ +package webhook + +import ( + "context" + + spinv1 "github.com/spinkube/spin-operator/api/v1" + "github.com/spinkube/spin-operator/internal/logging" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// nolint:lll +//+kubebuilder:webhook:path=/validate-core-spinoperator-dev-v1-spinappexecutor,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.spinoperator.dev,resources=spinappexecutors,verbs=create;update,versions=v1,name=vspinappexecutor.kb.io,admissionReviewVersions=v1 + +// SpinAppExecutorValidator validates SpinApps +type SpinAppExecutorValidator struct { + Client client.Client +} + +// ValidateCreate implements webhook.Validator +func (v *SpinAppExecutorValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + executor := obj.(*spinv1.SpinAppExecutor) + log.Info("validate create", "name", executor.Name) + + return nil, v.validateSpinAppExecutor(executor) +} + +// ValidateUpdate implements webhook.Validator +func (v *SpinAppExecutorValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + executor := newObj.(*spinv1.SpinAppExecutor) + log.Info("validate update", "name", executor.Name) + + return nil, v.validateSpinAppExecutor(executor) +} + +// ValidateDelete implements webhook.Validator +func (v *SpinAppExecutorValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + log := logging.FromContext(ctx) + + executor := obj.(*spinv1.SpinAppExecutor) + log.Info("validate delete", "name", executor.Name) + + return nil, nil +} + +func (v *SpinAppExecutorValidator) validateSpinAppExecutor(executor *spinv1.SpinAppExecutor) error { + return nil +} diff --git a/internal/webhook/spinappexecutor_validating_test.go b/internal/webhook/spinappexecutor_validating_test.go new file mode 100644 index 00000000..28bed6e9 --- /dev/null +++ b/internal/webhook/spinappexecutor_validating_test.go @@ -0,0 +1,3 @@ +package webhook + +// Currently the validating webhook is a no-op so nothing to test diff --git a/pkg/spinapp/spinapp.go b/pkg/spinapp/spinapp.go new file mode 100644 index 00000000..a005e6ed --- /dev/null +++ b/pkg/spinapp/spinapp.go @@ -0,0 +1,37 @@ +package spinapp + +import ( + "fmt" + + "github.com/spinkube/spin-operator/internal/constants" +) + +const ( + // HTTPPortName is the name used to identify the HTTP Port on a spin app + // deployment. + HTTPPortName = "http-app" + + // DefaultHTTPPort is the port that the operator will assign to a pod by + // default when constructing deployments and services. + DefaultHTTPPort = 80 + + // StatusReady is the ready value for an app status label. + StatusReady = "ready" +) + +var ( + // NameLabelKey is the app name label key. + NameLabelKey = constants.ConstructResourceLabelKey("app-name") +) + +// ConstructStatusLabelKey returns the app status label key, used primarily +// in Service selectors. +func ConstructStatusLabelKey(appName string) string { + return constants.ConstructResourceLabelKey(fmt.Sprintf("app.%s.status", appName)) +} + +// ConstructStatusReadyLabel returns the app status label key and value used +// by a Service selector to select Pod(s) ready to serve an app. +func ConstructStatusReadyLabel(appName string) (string, string) { + return ConstructStatusLabelKey(appName), StatusReady +} diff --git a/result b/result new file mode 120000 index 00000000..08635b42 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/n75zsjb4mqsg2daqypgjpn6787y3w01z-spin-operator-dev \ No newline at end of file diff --git a/spin-runtime-class.yaml b/spin-runtime-class.yaml new file mode 100644 index 00000000..91cfc7c9 --- /dev/null +++ b/spin-runtime-class.yaml @@ -0,0 +1,5 @@ +apiVersion: node.k8s.io/v1 +kind: RuntimeClass +metadata: + name: wasmtime-spin-v2 +handler: spin \ No newline at end of file