├── .devcontainer.json ├── .dockerignore ├── .envrc ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── build.yaml │ ├── docs.yaml │ ├── e2e.yaml │ ├── helm-install-smoketest.yaml │ ├── publish.yaml │ ├── sample-apps.yaml │ └── smoketest.yaml ├── .gitignore ├── .golangci.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── RELEASE.md ├── SECURITY.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── spinapp_types.go │ ├── spinappexecutor_types.go │ └── zz_generated.deepcopy.go ├── apps ├── README.md ├── cpu-load-gen │ ├── .gitignore │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── hello-world │ ├── .gitignore │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── order-processor │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── outbound-http │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── redis-sample │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── salutations │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── variable-explorer │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── spin.toml │ └── src │ │ └── lib.rs └── variabletester │ ├── .gitignore │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── spin.toml ├── charts └── spin-operator │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── leader-election-rbac.yaml │ ├── manager-rbac.yaml │ ├── metrics-auth-rbac.yaml │ ├── metrics-reader-rbac.yaml │ ├── metrics-role-and-binding.yaml │ ├── metrics-service.yaml │ ├── mutating-webhook-configuration.yaml │ ├── selfsigned-issuer.yaml │ ├── serviceaccount.yaml │ ├── serving-cert.yaml │ ├── validating-webhook-configuration.yaml │ └── webhook-service.yaml │ └── values.yaml ├── cmd └── main.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── chart │ ├── README.md │ └── values.yaml ├── crd │ ├── bases │ │ ├── core.spinkube.dev_spinappexecutors.yaml │ │ └── core.spinkube.dev_spinapps.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_spinappexecutors.yaml │ │ ├── cainjection_in_spinapps.yaml │ │ ├── webhook_in_spinappexecutors.yaml │ │ └── webhook_in_spinapps.yaml ├── default │ ├── kustomization.yaml │ ├── manager_config_patch.yaml │ ├── manager_metrics_patch.yaml │ ├── manager_webhook_patch.yaml │ ├── metrics_service.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── kustomization.yml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_reader_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── service_account.yaml │ ├── spinapp_editor_role.yaml │ ├── spinapp_viewer_role.yaml │ ├── spinappexecutor_editor_role.yaml │ └── spinappexecutor_viewer_role.yaml ├── samples │ ├── annotations.yaml │ ├── hpa.yaml │ ├── keda-app.yaml │ ├── keda-scaledobject.yaml │ ├── kustomization.yaml │ ├── otel.yaml │ ├── private-image.yaml │ ├── probes.yaml │ ├── redis.yaml │ ├── resources.yaml │ ├── runtime-config.yaml │ ├── selective-deployment.yaml │ ├── simple.yaml │ ├── spin-runtime-class.yaml │ ├── spin-shim-executor.yaml │ ├── spintainer-executor.yaml │ ├── spintainer.yaml │ ├── variable-explorer.yaml │ ├── variables.yaml │ └── volume-mount.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── e2e ├── README.md ├── crd_installed_test.go ├── default_test.go ├── helper │ └── helper.go ├── k3d_provider.go ├── main_test.go ├── metrics_server_test.go ├── redis_test.go ├── selective_deployment_test.go └── spintainer_test.go ├── flake.lock ├── flake.nix ├── format.Dockerfile ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── provision-minikube.sh └── runtime-config-to-secret.sh ├── internal ├── cacerts │ ├── ca-certificates.crt │ └── cacerts.go ├── constants │ └── constants.go ├── controller │ ├── deployment.go │ ├── deployment_test.go │ ├── service.go │ ├── service_test.go │ ├── spinapp_controller.go │ ├── spinapp_controller_test.go │ ├── spinappexecutor_controller.go │ └── spinappexecutor_controller_test.go ├── generics │ └── generics.go ├── logging │ └── logr_logger.go ├── runtimeconfig │ ├── runtime_config.go │ ├── runtime_config_test.go │ └── types.go └── webhook │ ├── admission.go │ ├── admission_test.go │ ├── spinapp_defaulting.go │ ├── spinapp_defaulting_test.go │ ├── spinapp_validating.go │ ├── spinapp_validating_test.go │ ├── spinappexecutor_defaulting.go │ ├── spinappexecutor_defaulting_test.go │ ├── spinappexecutor_validating.go │ └── spinappexecutor_validating_test.go ├── pkg ├── secret │ ├── secret.go │ └── secret_test.go └── spinapp │ └── spinapp.go └── scripts └── update-chart-versions.sh /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 3 | "features": { 4 | "ghcr.io/devcontainers/features/go:1": { 5 | "version": "1.23.0" 6 | }, 7 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 8 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { 9 | "version": "latest", 10 | "helm": "latest", 11 | "minikube": "none" 12 | } 13 | }, 14 | "containerEnv": { 15 | "E2E_USE_NATIVE_SNAPSHOTTER": "true" 16 | }, 17 | "customizations": { 18 | "vscode": { 19 | "settings": { 20 | "go.toolsManagement.checkForUpdates": "local", 21 | "go.useLanguageServer": true, 22 | "go.gopath": "/go" 23 | }, 24 | "extensions": [ 25 | "golang.Go", 26 | "DavidAnson.vscode-markdownlint" 27 | ] 28 | } 29 | }, 30 | // Allow Debuggers to work 31 | "capAdd": [ 32 | "SYS_PTRACE" 33 | ], 34 | "securityOpt": [ 35 | "seccomp=unconfined" 36 | ] 37 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | 4 | # NOTE: Order is important; the last matching pattern takes the most precedence. When someone opens a pull request that 5 | # only modifies files under a certain matching pattern, only those code owners will be requested for a review. 6 | 7 | # These owners will be the default owners for everything in the repository. Unless a later match takes precedence, they 8 | # will be requested for review when someone opens a pull request. 9 | * @calebschoepp @endocrimes @bacongobbler @michelleN @rajatjindal 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Go Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | packages: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: '1.23.x' 23 | cache: true 24 | 25 | - name: Setup gotestsum 26 | uses: autero1/action-gotestsum@v2.0.0 27 | with: 28 | gotestsum_version: "1.8.2" 29 | 30 | - name: Install dependencies 31 | run: go mod download 32 | 33 | - name: Build 34 | run: CGO_ENABLED=0 go build -v ./... 35 | 36 | - name: Setup EnvTest 37 | run: make envtest 38 | 39 | - name: Test 40 | run: | 41 | mkdir .results 42 | gotestsum \ 43 | --junitfile .results/results.xml \ 44 | --jsonfile .results/results.json \ 45 | --format testname \ 46 | -- -coverprofile=.results/cover.out $(go list ./... | grep -v e2e) 47 | 48 | - name: Test Summary 49 | uses: test-summary/action@v2 50 | with: 51 | paths: ".results/results.xml" 52 | if: always() 53 | - name: Upload test results 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: results.xml 57 | path: ./.results/results.xml 58 | if: always() 59 | - name: Upload test coverage 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: cover.out 63 | path: ./.results/cover.out 64 | if: always() 65 | - name: Upload Go test results json 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: results.json 69 | path: ./.results/results.json 70 | 71 | lint_go: 72 | name: lint go 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/setup-go@v5 76 | with: 77 | go-version: '1.23.x' 78 | - uses: actions/checkout@v4 79 | - name: golangci-lint 80 | uses: golangci/golangci-lint-action@v6 81 | with: 82 | version: v1.60.1 83 | args: --timeout=10m 84 | 85 | lint_shell: 86 | name: lint shell 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v4 90 | - name: Run ShellCheck 91 | uses: ludeeus/action-shellcheck@master 92 | 93 | lint_chart: 94 | name: lint chart 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: actions/checkout@v4 98 | - name: Setup Go 99 | uses: actions/setup-go@v5 100 | with: 101 | go-version: '1.23.x' 102 | cache: true 103 | - name: Install dependencies 104 | run: go mod download 105 | - name: Install helm 106 | uses: Azure/setup-helm@v4 107 | with: 108 | version: v3.14.0 109 | - name: Lint chart 110 | run: make helm-lint 111 | 112 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint-markdown: 11 | name: Lint all markdown files 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | 22 | - name: Lint markdown 23 | run: | 24 | make lint-markdown 25 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | e2e: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: "1.23.x" 16 | cache: true 17 | - name: run e2e 18 | run: go test ./e2e -v 19 | -------------------------------------------------------------------------------- /.github/workflows/helm-install-smoketest.yaml: -------------------------------------------------------------------------------- 1 | name: Helm Install Smoketest 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | helm-install-smoke-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.23.x" 18 | 19 | - name: Install helm 20 | uses: Azure/setup-helm@v4 21 | with: 22 | version: v3.14.0 23 | 24 | - name: setup k3d 25 | uses: engineerd/configurator@v0.0.10 26 | with: 27 | name: k3d 28 | url: https://github.com/k3d-io/k3d/releases/download/v5.6.0/k3d-linux-amd64 29 | 30 | - name: create spin-operator docker image 31 | run: make docker-build IMG=spin-operator:latest 32 | 33 | - name: start k3d cluster 34 | run: | 35 | k3d cluster create wasm-cluster \ 36 | --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.14.0 \ 37 | --port "8081:80@loadbalancer" \ 38 | --agents 2 39 | 40 | - name: import operator image into k3d cluster 41 | run: k3d image import -c wasm-cluster spin-operator:latest 42 | 43 | - name: helm install cert-manager 44 | run: | 45 | helm repo add jetstack https://charts.jetstack.io 46 | helm install cert-manager jetstack/cert-manager \ 47 | --namespace cert-manager \ 48 | --create-namespace \ 49 | --version v1.13.3 \ 50 | --set installCRDs=true 51 | 52 | - name: install crds 53 | run: make install 54 | 55 | - name: helm install spin-operator 56 | run: | 57 | make helm-install IMG=spin-operator:latest HELM_EXTRA_ARGS=--debug 58 | 59 | - name: create containerd-shim-spin executor 60 | run: kubectl create -f config/samples/spin-shim-executor.yaml 61 | 62 | - name: create runtime class 63 | run: kubectl create -f config/samples/spin-runtime-class.yaml 64 | 65 | - name: debug 66 | if: failure() 67 | run: | 68 | kubectl get pods -A 69 | kubectl get pods -n spin-operator 70 | kubectl get certificate -n spin-operator 71 | kubectl logs -n spin-operator $(kubectl get pods -n spin-operator | grep spin-operator-controller-manager | awk '{print $1}') || true 72 | kubectl describe -n spin-operator pod $(kubectl get pods -n spin-operator | grep spin-operator-controller-manager | awk '{print $1}') || true 73 | kubectl logs -n spin-operator $(kubectl get pods -n spin-operator | grep wait-for-webhook-svc | awk '{print $1}') || true 74 | kubectl describe -n spin-operator pod $(kubectl get pods -n spin-operator | grep wait-for-webhook-svc | awk '{print $1}') || true 75 | 76 | - name: run spin app 77 | run: | 78 | kubectl apply -f config/samples/simple.yaml 79 | kubectl rollout status deployment simple-spinapp --timeout 90s 80 | kubectl get pods -A 81 | kubectl port-forward svc/simple-spinapp 8083:80 & 82 | timeout 15s bash -c 'until curl -f -vvv http://localhost:8083/hello; do sleep 2; done' 83 | 84 | - name: Verify curl 85 | run: curl localhost:8083/hello 86 | -------------------------------------------------------------------------------- /.github/workflows/sample-apps.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Sample Apps 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | publish-image: 13 | name: Publish sample app images 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | app: [cpu-load-gen, hello-world, outbound-http, variabletester, redis-sample, salutations] 18 | env: 19 | IMAGE_NAME: ${{ github.repository }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set the release version 25 | shell: bash 26 | run: | 27 | echo "RELEASE_VERSION=$(date +%Y%m%d-%H%M%S)-g$(git rev-parse --short HEAD)" >> $GITHUB_ENV 28 | 29 | - name: Install Spin 30 | uses: fermyon/actions/spin/setup@v1 31 | 32 | - name: Install TinyGo 33 | uses: acifani/setup-tinygo@v2 34 | with: 35 | tinygo-version: '0.33.0' 36 | 37 | - name: Build and push versioned image 38 | uses: fermyon/actions/spin/push@v1 39 | with: 40 | registry: ${{ env.REGISTRY }} 41 | registry_username: ${{ github.actor }} 42 | registry_password: ${{ secrets.GITHUB_TOKEN }} 43 | registry_reference: "ghcr.io/${{ github.repository_owner }}/spin-operator/${{ matrix.app }}:${{ env.RELEASE_VERSION }}" 44 | manifest_file: apps/${{ matrix.app }}/spin.toml 45 | -------------------------------------------------------------------------------- /.github/workflows/smoketest.yaml: -------------------------------------------------------------------------------- 1 | name: Smoketest 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.23.x" 18 | 19 | - name: setup k3d 20 | uses: engineerd/configurator@v0.0.10 21 | with: 22 | name: k3d 23 | url: https://github.com/k3d-io/k3d/releases/download/v5.6.0/k3d-linux-amd64 24 | 25 | - name: start k3d cluster 26 | run: | 27 | k3d cluster create wasm-cluster \ 28 | --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.14.0 \ 29 | --port "8081:80@loadbalancer" \ 30 | --agents 2 31 | 32 | - name: apply runtime class 33 | run: kubectl apply -f config/samples/spin-runtime-class.yaml 34 | 35 | - name: start controller 36 | timeout-minutes: 5 37 | run: | 38 | make install 39 | make run & 40 | 41 | timeout 300s bash -c 'until curl -s http://localhost:8082/healthz; do echo "waiting for controller to start"; sleep 2; done' 42 | echo "" 43 | echo "controller started successfully" 44 | 45 | - name: run spin app 46 | run: | 47 | kubectl apply -f config/samples/spin-shim-executor.yaml 48 | kubectl apply -f config/samples/simple.yaml 49 | kubectl rollout status deployment simple-spinapp --timeout 90s 50 | 51 | kubectl port-forward svc/simple-spinapp 8083:80 & 52 | timeout 15s bash -c 'until curl -f -vvv http://localhost:8083/hello; do sleep 2; done' 53 | 54 | - name: Verify curl 55 | run: curl localhost:8083/hello 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/* 9 | Dockerfile.cross 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | .vscode 24 | *.swp 25 | *.swo 26 | *~ 27 | .direnv/ 28 | 29 | # Helm chart dependencies 30 | 31 | charts/spin-operator/charts 32 | 33 | # Staging dir for packaging/releasing 34 | _dist -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | issues: 2 | # The default exclude list seems rather aggressive, opt-in when needed instead 3 | exclude-use-default: false 4 | 5 | exclude-rules: 6 | # Duplicated errcheck checks 7 | - linters: [gosec] 8 | text: G104 9 | # Duplicated errcheck checks 10 | - linters: [staticcheck] 11 | text: SA5001 12 | # We don't require comments on everything 13 | - linters: [golint] 14 | text: should have( a package)? comment 15 | # very long lines are ok if they're URLs 16 | - linters: [lll] 17 | source: https?:// 18 | # very long lines are ok if they're go:generate 19 | - linters: [lll] 20 | source: "^//go:generate " 21 | # Ignore errcheck on deferred Close 22 | - linters: [errcheck] 23 | source: ^\s*defer .*\.Close(.*)$ 24 | # Ignore ineffective assignments to ctx 25 | - linters: [ineffassign] 26 | source: ^\s*ctx.*=.*$ 27 | - linters: [staticcheck] 28 | source: ^\s*ctx.*=.*$ 29 | # Don't require package docs 30 | - linters: [stylecheck] 31 | text: ST1000 32 | # Unparam is allowed in tests 33 | - linters: [unparam] 34 | path: _test\.go 35 | 36 | linters: 37 | disable-all: true 38 | enable: 39 | - bodyclose 40 | - depguard 41 | - errcheck 42 | - errorlint 43 | - goconst 44 | - gocyclo 45 | - gofmt 46 | - goimports 47 | - gosec 48 | - gosimple 49 | - govet 50 | - ineffassign 51 | - lll 52 | - misspell 53 | - nakedret 54 | - staticcheck 55 | - stylecheck 56 | - typecheck 57 | - unconvert 58 | - unparam 59 | - unused 60 | - forbidigo 61 | 62 | linters-settings: 63 | govet: 64 | disable: 65 | - shadow 66 | gocyclo: 67 | min-complexity: 15 68 | dupl: 69 | # Don't detect small duplications, but if we're duplicating functions across 70 | # packages, we should consider refactoring. 71 | threshold: 100 72 | depguard: 73 | rules: 74 | main: 75 | files: 76 | - '$all' 77 | deny: 78 | - pkg: "github.com/pkg/errors" 79 | desc: "use Go 1.13 errors instead: https://blog.golang.org/go1.13-errors" 80 | testing: 81 | files: ['$test'] 82 | deny: 83 | - pkg: "github.com/stretchr/testify/assert" 84 | desc: "use github.com/stretchr/testify/require instead" 85 | goconst: 86 | min-len: 8 87 | min-occurrences: 10 88 | lll: 89 | line-length: 180 90 | forbidigo: 91 | # Forbid the following identifiers (list of regexp). 92 | # Default: ["^(fmt\\.Print(|f|ln)|print|println)$"] 93 | forbid: 94 | # Builtin function: 95 | - ^print.*$ 96 | - p: ^fmt\.Print.*$ 97 | msg: Do not commit print statements. 98 | - p: ^os\.Getenv 99 | msg: Pull values through configuration rather than os.Getenv 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project subscribes to the SpinKube [Code of Conduct](https://github.com/spinkube/governance/blob/main/CODE_OF_CONDUCT.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We are delighted that you are interested in making spin-operator better! Thank you! This document will guide you through 4 | making your first contribution to the project. We welcome and appreciate contributions of all types - opening issues, 5 | fixing typos, adding examples, one-liner code fixes, tests, or complete features. 6 | 7 | First, any contribution and interaction on any SpinKube project MUST follow our [Code of 8 | Conduct](https://github.com/spinkube/governance/blob/main/CODE_OF_CONDUCT.md). Thank you for being part of an inclusive and open community! 9 | 10 | If you plan on contributing anything complex, please go through the [open 11 | issues](https://github.com/spinkube/spin-operator/issues) and [PR queue](https://github.com/spinkube/spin-operator/pulls) 12 | first to make sure someone else has not started working on it. If it doesn't exist already, please [open an 13 | issue](https://github.com/spinkube/spin-operator/issues/new) so you have a chance to get feedback from the community and 14 | the maintainers before you start working on your feature. 15 | 16 | ## Making Code Contributions to spin-operator 17 | 18 | The following guide is intended to make sure your contribution can get merged as soon as possible. First, make sure you 19 | have the following prerequisites configured: 20 | 21 | - `go` version v1.22.0+ 22 | - `docker` version 17.03+ 23 | - `kubectl` version v1.28.0+ 24 | - Access to a Kubernetes v1.28.0+ cluster 25 | - `make` 26 | - please ensure you [configure adding a GPG signature to your 27 | commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) 28 | as well as appending a sign-off message (`git commit -S -s`) 29 | 30 | Once you have set up the prerequisites and identified the contribution you want to make to spin-operator, make sure you 31 | can correctly build the project: 32 | 33 | ```console 34 | # clone the repository 35 | $ git clone https://github.com/spinkube/spin-operator && cd spin-operator 36 | # add a new remote pointing to your fork of the project 37 | $ git remote add fork https://github.com//spin-operator 38 | # create a new branch for your work 39 | $ git checkout -b 40 | 41 | # build spin-operator 42 | $ make 43 | 44 | # make sure compilation is successful 45 | $ ./bin/manager --help 46 | 47 | # run the tests and make sure they pass 48 | $ make test 49 | ``` 50 | 51 | Now you should be ready to start making your contribution. To familiarize yourself with the spin-operator project, 52 | please read the [README](https://github.com/spinkube/spin-operator). Since most of spin-operator is written in Go, we try 53 | to follow the common Go coding conventions. If applicable, add unit or integration tests to ensure your contribution is 54 | correct. 55 | 56 | ## Before You Commit 57 | 58 | - Format the code (`go fmt ./...`) 59 | - Run Clippy (`go vet ./...`) 60 | - Run the lint task (`make lint` or `make lint-fix`) 61 | - Build the project and run the tests (`make test`) 62 | 63 | spin-operator enforces lints and tests as part of continuous integration - running them locally will save you a 64 | round-trip to your pull request! 65 | 66 | If everything works locally, you're ready to commit your changes. 67 | 68 | ## Committing and Pushing Your Changes 69 | 70 | We require commits to be signed both with an email address and with a GPG signature. 71 | 72 | > Because of the way GitHub runs enforcement, the GPG signature isn't checked until after all tests have run. Be sure to 73 | > GPG sign up front, as it can be a bit frustrating to wait for all the tests and then get blocked on the signature! 74 | 75 | ```console 76 | $ git commit -S -s -m "" 77 | ``` 78 | 79 | Some contributors like to follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) convention 80 | for commit messages. 81 | 82 | We try to only keep useful changes as separate commits - if you prefer to commit often, please cleanup the commit 83 | history before opening a pull request. 84 | 85 | Once you are happy with your changes you can push the branch to your fork: 86 | 87 | ```console 88 | # "fork" is the name of the git remote pointing to your fork 89 | $ git push fork 90 | ``` 91 | 92 | Now you are ready to create a pull request. Thank you for your contribution! 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build the manager binary 4 | FROM --platform=${BUILDPLATFORM} golang:1.23 AS builder 5 | ARG TARGETOS 6 | ARG TARGETARCH 7 | 8 | WORKDIR /workspace 9 | # Copy the Go Modules manifests 10 | COPY go.mod go.sum ./ 11 | 12 | # cache deps before building and copying source so that we don't need to re-download as much 13 | # and so that source changes don't invalidate our downloaded layer 14 | RUN go mod download 15 | 16 | # Copy the go source 17 | COPY . . 18 | 19 | # Don't set a fallback value for TARGETARCH so that it defaults to the GOARCH default 20 | # equivalent to `BUILDPLATFORM` - this ensures that `docker build .` will build for 21 | # the users local arch. 22 | RUN --mount=type=cache,target=/root/.cache/go-build \ 23 | CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make golangci-build 24 | 25 | # Use distroless as minimal base image to package the manager binary 26 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 27 | FROM gcr.io/distroless/static:nonroot 28 | WORKDIR / 29 | COPY --from=builder /workspace/bin/manager . 30 | USER 65532:65532 31 | 32 | ENTRYPOINT ["/manager"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) The Spin Framework Contributors. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: spinkube.dev 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: spin-operator 9 | repo: github.com/spinkube/spin-operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: spinkube.dev 16 | group: core 17 | kind: SpinApp 18 | path: github.com/spinkube/spin-operator/api/v1alpha1 19 | version: v1alpha1 20 | webhooks: 21 | defaulting: true 22 | validation: true 23 | webhookVersion: v1 24 | - api: 25 | crdVersion: v1 26 | namespaced: true 27 | controller: true 28 | domain: spinkube.dev 29 | group: core 30 | kind: SpinAppExecutor 31 | path: github.com/spinkube/spin-operator/api/v1alpha1 32 | version: v1alpha1 33 | webhooks: 34 | defaulting: true 35 | validation: true 36 | webhookVersion: v1 37 | version: "3" 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spin Operator 2 | 3 | Spin Operator enables deploying Spin applications to Kubernetes. It watches [SpinApp Custom 4 | Resources](https://www.spinkube.dev/docs/glossary/#spinapp-crd) and realizes the desired state in 5 | the Kubernetes cluster. 6 | 7 | This project was built using the Kubebuilder framework and contains a Spin App CRD and controller. 8 | 9 | All documentation is available online at https://www.spinkube.dev/docs/. If you're just getting 10 | started, the [quickstart guide](https://www.spinkube.dev/docs/install/quickstart/) will guide you to 11 | a minimal installation that'll work while you walk through the introduction. 12 | 13 | To get more help: 14 | 15 | - Join the #spinkube channel on Slack at https://cncf.slack.com. 16 | 17 | To contribute to SpinKube, check out the [contributing 18 | guide](https://www.spinkube.dev/docs/contrib/) for information about getting involved. 19 | 20 | ## Running the test suite 21 | 22 | To run the test suite, execute the following command: 23 | 24 | ```shell 25 | make test 26 | ``` 27 | 28 | ## Building 29 | 30 | To build the Spin Operator binary, execute the following command: 31 | 32 | ```shell 33 | make 34 | ``` 35 | 36 | ## Running a local development environment 37 | 38 | There are two options to run spin-operator: 39 | 40 | 1. Run spin-operator on your computer 41 | 1. Deploy spin-operator to a remote Kubernetes cluster 42 | 43 | ### Option 1: Run spin-operator on your computer 44 | 45 | k3d is a lightweight Kubernetes distribution that runs on Docker. This is the standard development 46 | workflow most spin-operator developers use to test their changes. 47 | 48 | Ensure that your system has all the prerequisites installed before continuing: 49 | 50 | - [Go](https://go.dev/) 51 | - [Docker](https://docs.docker.com/engine/install/) 52 | - [k3d](https://k3d.io/) 53 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 54 | 55 | Create a k3d cluster: 56 | 57 | ```shell 58 | k3d cluster create wasm-cluster \ 59 | --image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.16.0 \ 60 | -p "8081:80@loadbalancer" \ 61 | --agents 2 62 | ``` 63 | 64 | Install the `SpinApp` and `SpinAppExecutor` Custom Resource Definitions into the cluster: 65 | 66 | ```shell 67 | make install 68 | ``` 69 | 70 | Create a `RuntimeClass` and `SpinAppExecutor`: 71 | 72 | ```shell 73 | kubectl apply -f config/samples/spin-runtime-class.yaml 74 | kubectl apply -f config/samples/spin-shim-executor.yaml 75 | ``` 76 | 77 | Run spin-operator: 78 | 79 | ```shell 80 | make run 81 | ``` 82 | 83 | Run the sample application: 84 | 85 | ```shell 86 | kubectl apply -f ./config/samples/simple.yaml 87 | ``` 88 | 89 | Forward a local port to the application so that it can be reached: 90 | 91 | ```shell 92 | kubectl port-forward svc/simple-spinapp 8083:80 93 | ``` 94 | 95 | In a different terminal window, make a request to the application: 96 | 97 | ```shell 98 | curl localhost:8083/hello 99 | ``` 100 | 101 | You should see "Hello world from Spin!". 102 | 103 | ### Option 2: Deploy spin-operator to a remote Kubernetes cluster 104 | 105 | This is harder than running Spin Operator on your computer, but deploying Spin Operator into a 106 | remote cluster lets you test things like webhook support. 107 | 108 | Ensure that your system has all the prerequisites installed before continuing: 109 | 110 | - [Go](https://go.dev/) 111 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 112 | - [Docker](https://docs.docker.com/engine/install/) (optional: for building and pushing your own 113 | Docker image) 114 | 115 | Install cert-manager into your cluster for webhook support: 116 | 117 | ```shell 118 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.3/cert-manager.yaml 119 | kubectl wait --for=condition=available --timeout=300s deployment/cert-manager-webhook -n cert-manager 120 | ``` 121 | 122 | Install the `SpinApp` and `SpinAppExecutor` Custom Resource Definitions into the cluster: 123 | 124 | ```shell 125 | make install 126 | ``` 127 | 128 | Create a `RuntimeClass` and `SpinAppExecutor`: 129 | 130 | ```shell 131 | kubectl apply -f config/samples/spin-runtime-class.yaml 132 | kubectl apply -f config/samples/spin-shim-executor.yaml 133 | ``` 134 | 135 | > OPTIONAL: You can build and push the Spin Operator image using `make docker-build` and 136 | > `make docker-push`. 137 | > 138 | > export IMG_REPO=/spin-operator 139 | > make docker-build docker-push 140 | 141 | Deploy Spin Operator to the cluster: 142 | 143 | ```shell 144 | make deploy 145 | ``` 146 | 147 | Run the sample application: 148 | 149 | ```shell 150 | kubectl apply -f ./config/samples/simple.yaml 151 | ``` 152 | 153 | Forward a local port to the application so that it can be reached: 154 | 155 | ```shell 156 | kubectl port-forward svc/simple-spinapp 8083:80 157 | ``` 158 | 159 | In a different terminal window, make a request to the application: 160 | 161 | ```shell 162 | curl localhost:8083/hello 163 | ``` 164 | 165 | You should see "Hello world from Spin!". 166 | 167 | > NOTE: If you encounter RBAC errors, you may need to grant yourself cluster-admin privileges or be 168 | > logged in as admin. 169 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # spin-operator release process 2 | 3 | The vast majority of the release process is handled after you push a new git tag. 4 | 5 | First, let's start by setting some environment variables we can reference later. 6 | 7 | ```console 8 | export TAG=v0.x.0 #CHANGEME 9 | ``` 10 | 11 | To push a tag, do the following: 12 | 13 | ```console 14 | git checkout main 15 | git remote add upstream git@github.com:spinkube/spin-operator 16 | git pull upstream main 17 | git tag --sign $TAG --message "Release $TAG" 18 | git push upstream $TAG 19 | ``` 20 | 21 | Observe that the [CI run for that tag](https://github.com/spinkube/spin-operator/actions) completed. 22 | 23 | Bump the Helm chart versions. See #311 for an example. 24 | 25 | Next, you'll need to update the documentation: 26 | 27 | ```console 28 | git clone git@github.com:spinkube/documentation 29 | cd documentation 30 | ``` 31 | 32 | Change all references from the previous version to the new version. 33 | 34 | Contribute those changes and open a PR. 35 | 36 | As an optional step, you can run a set of smoke tests to ensure the latest release works as expected. 37 | 38 | Finally, announce the new release on the #spinkube CNCF Slack channel. 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting a Vulnerability 4 | 5 | The Spin Framework team and community take security vulnerabilities very seriously. If you find a security related issue with Spin Operator, we kindly ask you to report it [through the GitHub project](https://github.com/spinframework/spin-operator/security). All reports will be thoroughly investigated by the Spin Framework maintainers. 6 | 7 | ## Disclosure 8 | 9 | We will disclose any security vulnerabilities in our [Security Advisories](https://github.com/spinframework/spin-operator/security/advisories). 10 | 11 | All vulnerabilities and associated information will be treated with full confidentiality. We are thankful for your efforts in keeping the Spin Operator secure, and we will publicly acknowledge your contributions if you wish. 12 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the spin v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=core.spinkube.dev 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "core.spinkube.dev", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/spinappexecutor_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // SpinAppExecutorSpec defines the desired state of SpinAppExecutor 27 | type SpinAppExecutorSpec struct { 28 | // CreateDeployment specifies whether the Executor wants the SpinKube operator 29 | // to create a deployment for the application or if it will be realized externally. 30 | CreateDeployment bool `json:"createDeployment"` 31 | 32 | // DeploymentConfig specifies how the deployment should be configured when 33 | // createDeployment is true. 34 | DeploymentConfig *ExecutorDeploymentConfig `json:"deploymentConfig,omitempty"` 35 | } 36 | 37 | type ExecutorDeploymentConfig struct { 38 | // RuntimeClassName is the runtime class name that should be used by pods created 39 | // as part of a deployment. This should only be defined when SpintainerImage is not defined. 40 | RuntimeClassName *string `json:"runtimeClassName,omitempty"` 41 | 42 | // SpinImage points to an image that will run Spin in a container to execute 43 | // your SpinApp. This is an alternative to using the shim to execute your 44 | // SpinApp. This should only be defined when RuntimeClassName is not 45 | // defined. When specified, application images must be available without 46 | // authentication. 47 | SpinImage *string `json:"spinImage,omitempty"` 48 | 49 | // CACertSecret specifies the name of the secret containing the CA 50 | // certificates to be mounted to the deployment. 51 | CACertSecret string `json:"caCertSecret,omitempty"` 52 | 53 | // InstallDefaultCACerts specifies whether the default CA 54 | // certificate bundle should be generated. When set a new secret 55 | // will be created containing the certificates. If no secret name is 56 | // defined in `CACertSecret` the secret name will be `spin-ca`. 57 | InstallDefaultCACerts bool `json:"installDefaultCACerts,omitempty"` 58 | 59 | // Otel provides Kubernetes Bindings to Otel Variables. 60 | Otel *OtelConfig `json:"otel,omitempty"` 61 | } 62 | 63 | // SpinAppExecutorStatus defines the observed state of SpinAppExecutor 64 | type SpinAppExecutorStatus struct { 65 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 66 | // Important: Run "make" to regenerate code after modifying this file 67 | } 68 | 69 | // OtelConfig is the supported environment variables for OpenTelemetry 70 | type OtelConfig struct { 71 | // ExporterOtlpEndpoint configures the default combined otlp endpoint for sending telemetry 72 | ExporterOtlpEndpoint string `json:"exporter_otlp_endpoint,omitempty"` 73 | // ExporterOtlpTracesEndpoint configures the trace-specific otlp endpoint 74 | ExporterOtlpTracesEndpoint string `json:"exporter_otlp_traces_endpoint,omitempty"` 75 | // ExporterOtlpMetricsEndpoint configures the metrics-specific otlp endpoint 76 | ExporterOtlpMetricsEndpoint string `json:"exporter_otlp_metrics_endpoint,omitempty"` 77 | // ExporterOtlpLogsEndpoint configures the logs-specific otlp endpoint 78 | ExporterOtlpLogsEndpoint string `json:"exporter_otlp_logs_endpoint,omitempty"` 79 | } 80 | 81 | //+kubebuilder:object:root=true 82 | //+kubebuilder:subresource:status 83 | 84 | // SpinAppExecutor is the Schema for the spinappexecutors API 85 | type SpinAppExecutor struct { 86 | metav1.TypeMeta `json:",inline"` 87 | metav1.ObjectMeta `json:"metadata,omitempty"` 88 | 89 | Spec SpinAppExecutorSpec `json:"spec,omitempty"` 90 | Status SpinAppExecutorStatus `json:"status,omitempty"` 91 | } 92 | 93 | //+kubebuilder:object:root=true 94 | 95 | // SpinAppExecutorList contains a list of SpinAppExecutor 96 | type SpinAppExecutorList struct { 97 | metav1.TypeMeta `json:",inline"` 98 | metav1.ListMeta `json:"metadata,omitempty"` 99 | Items []SpinAppExecutor `json:"items"` 100 | } 101 | 102 | func init() { 103 | SchemeBuilder.Register(&SpinAppExecutor{}, &SpinAppExecutorList{}) 104 | } 105 | -------------------------------------------------------------------------------- /apps/README.md: -------------------------------------------------------------------------------- 1 | # Apps 2 | 3 | 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. 4 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/README.md: -------------------------------------------------------------------------------- 1 | # cpu-load-gen 2 | 3 | A simple Spin application that generates CPU load by calculating the fibonacci sequence. 4 | 5 | ## Development 6 | 7 | ```bash 8 | # Build it 9 | spin build 10 | 11 | # Run it 12 | spin up 13 | 14 | # Push it to registry to be used by SpinApp in spin-operator 15 | spin registry push ttl.sh/cpu-load-gen:1h 16 | ``` 17 | 18 | TODO: Spin build and publish this image 19 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cpu_load_gen 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da 6 | 7 | require github.com/julienschmidt/httprouter v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da h1:+VKaIRRCsRuKggpt7xDF5Euc0eSShnwLVFNC2evv2Qo= 2 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240116170232-bbbb59b821da/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | spinhttp "github.com/fermyon/spin/sdk/go/v2/http" 8 | ) 9 | 10 | func init() { 11 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 12 | x := 43 // Experimentally generates a reasonable amount of CPU load 13 | fmt.Printf("Calculating fib(%d)\n", x) 14 | fmt.Fprintf(w, "fib(%d) = %d\n", x, fib(x)) 15 | }) 16 | } 17 | 18 | func fib(n int) int { 19 | if n < 2 { 20 | return n 21 | } 22 | return fib(n-2) + fib(n-1) 23 | } 24 | 25 | func main() {} 26 | -------------------------------------------------------------------------------- /apps/cpu-load-gen/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "cpu-load-gen" 5 | version = "0.1.0" 6 | authors = ["Caleb Schoepp "] 7 | description = "A simple Spin app that will generate lots of load on the CPU by computing large fibonacci sequences" 8 | 9 | [[trigger.http]] 10 | route = "/..." 11 | component = "cpu-load-gen" 12 | 13 | [component.cpu-load-gen] 14 | source = "main.wasm" 15 | allowed_outbound_hosts = [] 16 | [component.cpu-load-gen.build] 17 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 18 | watch = ["**/*.go", "go.mod"] 19 | -------------------------------------------------------------------------------- /apps/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # hello-world 2 | 3 | This is a simple hello world Spin app to demonstrate running Spin in Kubernetes. 4 | -------------------------------------------------------------------------------- /apps/hello-world/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hello_world 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3 6 | 7 | require github.com/julienschmidt/httprouter v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /apps/hello-world/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3 h1:B69YRVsqBEdP23rPUyjFmz2BK73E42TCjqCa0d1RZ6M= 2 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240123162134-c4fcb8fc13c3/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /apps/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | spinhttp "github.com/fermyon/spin/sdk/go/v2/http" 8 | ) 9 | 10 | func init() { 11 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 12 | w.Header().Set("Content-Type", "text/plain") 13 | fmt.Fprintln(w, "Hello Fermyon!") 14 | }) 15 | } 16 | 17 | func main() {} 18 | -------------------------------------------------------------------------------- /apps/hello-world/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "hello-world" 5 | version = "0.1.0" 6 | authors = ["Caleb Schoepp "] 7 | description = "A simple hello world Spin app" 8 | 9 | [[trigger.http]] 10 | route = "/..." 11 | component = "hello-world" 12 | 13 | [component.hello-world] 14 | source = "main.wasm" 15 | allowed_outbound_hosts = [] 16 | [component.hello-world.build] 17 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 18 | watch = ["**/*.go", "go.mod"] 19 | -------------------------------------------------------------------------------- /apps/order-processor/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/order-processor/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/order_processor 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e 6 | -------------------------------------------------------------------------------- /apps/order-processor/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e h1:489XksQPuLJG5uBsJ8nBSOaoUYQI+s2/RM/lJV68+SQ= 2 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240118230251-c25b685ed49e/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | -------------------------------------------------------------------------------- /apps/order-processor/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/fermyon/spin/sdk/go/v2/redis" 9 | ) 10 | 11 | func init() { 12 | // redis.Handle() must be called in the init() function. 13 | redis.Handle(func(payload []byte) error { 14 | fmt.Println("Payload::::") 15 | fmt.Println(string(payload)) 16 | return nil 17 | }) 18 | } 19 | 20 | // main function must be included for the compiler but is not executed. 21 | func main() {} 22 | -------------------------------------------------------------------------------- /apps/order-processor/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "order-processor" 5 | version = "0.1.0" 6 | authors = ["Caleb Schoepp "] 7 | description = "Process orders off of redis queue" 8 | 9 | [application.trigger.redis] 10 | address = "redis://:txfM5aXAOe@redis-master.default.svc.cluster.local:6379" 11 | 12 | [[trigger.redis]] 13 | channel = "orders" 14 | component = "order-processor" 15 | 16 | [component.order-processor] 17 | source = "main.wasm" 18 | allowed_outbound_hosts = [] 19 | [component.order-processor.build] 20 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 21 | -------------------------------------------------------------------------------- /apps/outbound-http/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/outbound-http/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spinkube/spin-operator/outbound-http 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.0.0 6 | 7 | require ( 8 | github.com/fermyon/spin-go-sdk v0.0.0-20240220234050-48ddef7a2617 // indirect 9 | github.com/julienschmidt/httprouter v1.3.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /apps/outbound-http/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin-go-sdk v0.0.0-20240220234050-48ddef7a2617 h1:raijo8KV3rhhJDHCPhgeTEzViFSR4WUT+wgptVReoVY= 2 | github.com/fermyon/spin-go-sdk v0.0.0-20240220234050-48ddef7a2617/go.mod h1:9GoW1+MR0gN1OEinITtjPOzmu0dur3U6ty3pIH/gN24= 3 | github.com/fermyon/spin/sdk/go/v2 v2.0.0 h1:pMq2BxXio9gsBdPVNCuebCsLSt64yaTS3kV2l1gL088= 4 | github.com/fermyon/spin/sdk/go/v2 v2.0.0/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 5 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 6 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 7 | -------------------------------------------------------------------------------- /apps/outbound-http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | spinhttp "github.com/fermyon/spin-go-sdk/http" 8 | ) 9 | 10 | func init() { 11 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 12 | url := "https://random-data-api.fermyon.app/animals/json" 13 | resp, err := spinhttp.Get(url) 14 | if err != nil { 15 | http.Error(w, err.Error(), http.StatusInternalServerError) 16 | return 17 | } 18 | 19 | fmt.Fprintln(w, resp.Body) 20 | }) 21 | } 22 | 23 | func main() {} 24 | -------------------------------------------------------------------------------- /apps/outbound-http/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "outbound-http" 5 | authors = ["Fermyon Engineering "] 6 | description = "A simple Spin application written in (Tiny)Go that performs outbound HTTP requests." 7 | version = "1.0.0" 8 | 9 | [[trigger.http]] 10 | route = "/hello" 11 | component = "hello" 12 | 13 | [component.hello] 14 | source = "main.wasm" 15 | allowed_outbound_hosts = ["https://random-data-api.fermyon.app:443"] 16 | [component.hello.build] 17 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 18 | -------------------------------------------------------------------------------- /apps/redis-sample/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is an OCI-compliant package that can be used to demonstrate how a Spin app interacts with Redis in a Kubernetes cluster. 4 | 5 | # Usage 6 | 7 | ## Deploying the Spin app 8 | 9 | Create a Kubernetes manifest file named `redis_client.yaml` with the following code: 10 | 11 | ```yaml 12 | apiVersion: core.spinkube.dev/v1alpha1 13 | kind: SpinApp 14 | metadata: 15 | name: redis-spinapp 16 | spec: 17 | image: "ghcr.io/spinkube/redis-sample" 18 | replicas: 1 19 | executor: containerd-shim-spin 20 | variables: 21 | - name: redis_endpoint 22 | value: redis://redis.default.svc.cluster.local:6379 23 | ``` 24 | 25 | Once created, run `kubectl apply -f redis_client.yaml`. 26 | 27 | ## Deploying Redis 28 | 29 | Create a Kubernetes manifest file named `redis_db.yaml` with the following code: 30 | 31 | ```yaml 32 | apiVersion: apps/v1 33 | kind: Deployment 34 | metadata: 35 | name: redis 36 | labels: 37 | app: redis 38 | spec: 39 | replicas: 1 40 | selector: 41 | matchLabels: 42 | app: redis 43 | template: 44 | metadata: 45 | labels: 46 | app: redis 47 | spec: 48 | containers: 49 | - name: redis 50 | image: redis 51 | ports: 52 | - containerPort: 6379 53 | 54 | --- 55 | apiVersion: v1 56 | kind: Service 57 | metadata: 58 | name: redis 59 | spec: 60 | selector: 61 | app: redis 62 | ports: 63 | - protocol: TCP 64 | port: 6379 65 | targetPort: 6379 66 | ``` 67 | 68 | Once created, run `kubectl apply -f redis_db.yaml`. 69 | 70 | ## Interacting with the Spin app 71 | 72 | In your terminal run `kubectl port-forward svc/redis-spinapp 3000:80`, then in a different terminal window, try the below commands: 73 | 74 | ### Place a key-value pair in Redis 75 | 76 | ```bash 77 | curl --request PUT --data-binary "Hello, world\!" -H 'x-key: helloKey' localhost:3000 78 | ``` 79 | 80 | ### Retrieve a value from Redis 81 | 82 | ```bash 83 | curl -H 'x-key: helloKey' localhost:3000 84 | ``` 85 | 86 | ### Delete a value from Redis 87 | 88 | ```bash 89 | curl --request DELETE -H 'x-key: helloKey' localhost:3000 90 | ``` 91 | -------------------------------------------------------------------------------- /apps/redis-sample/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spin_redis_sample 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.2.0 6 | 7 | require github.com/julienschmidt/httprouter v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /apps/redis-sample/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.2.0 h1:zHZdIqjbUwyxiwdygHItnM+vUUNSZ3CX43jbIUemBI4= 2 | github.com/fermyon/spin/sdk/go/v2 v2.2.0/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /apps/redis-sample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | spinhttp "github.com/fermyon/spin/sdk/go/v2/http" 9 | "github.com/fermyon/spin/sdk/go/v2/redis" 10 | "github.com/fermyon/spin/sdk/go/v2/variables" 11 | ) 12 | 13 | var rdb *redis.Client 14 | 15 | func init() { 16 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 17 | redisEndpoint, err := variables.Get("redis_endpoint") 18 | if err != nil { 19 | http.Error(w, "unable to parse variable 'redis_endpoint'", http.StatusInternalServerError) 20 | return 21 | } 22 | 23 | if redisEndpoint == "" { 24 | http.Error(w, "cannot find 'redis_endpoint' environment variable", http.StatusInternalServerError) 25 | return 26 | } 27 | 28 | rdb = redis.NewClient(redisEndpoint) 29 | 30 | reqKey := r.Header.Get("x-key") 31 | if reqKey == "" { 32 | http.Error(w, "you must include the 'x-key' header in your request", http.StatusBadRequest) 33 | return 34 | } 35 | 36 | if r.Method == "GET" { 37 | value, err := rdb.Get(reqKey) 38 | if err != nil { 39 | http.Error(w, fmt.Sprintf("no value found for key '%s'", reqKey), http.StatusNotFound) 40 | return 41 | } 42 | 43 | w.WriteHeader(http.StatusOK) 44 | w.Write(value) 45 | return 46 | 47 | } else if r.Method == "PUT" { 48 | bodyBytes, err := io.ReadAll(r.Body) 49 | if err != nil { 50 | http.Error(w, fmt.Sprintf("error reading request body: %w", err), http.StatusInternalServerError) 51 | } 52 | defer r.Body.Close() 53 | 54 | if err := rdb.Set(reqKey, bodyBytes); err != nil { 55 | http.Error(w, fmt.Sprintf("unable to add value for key '%s' to database: %w", reqKey, err), http.StatusInternalServerError) 56 | return 57 | } 58 | 59 | w.WriteHeader(http.StatusCreated) 60 | return 61 | 62 | } else if r.Method == "DELETE" { 63 | _, err := rdb.Del(reqKey) 64 | if err != nil { 65 | http.Error(w, fmt.Sprintf("error deleting value for key '%w'", err), http.StatusInternalServerError) 66 | return 67 | } 68 | 69 | w.WriteHeader(http.StatusOK) 70 | return 71 | 72 | } else { 73 | http.Error(w, fmt.Sprintf("method %q is not supported, so please try again using 'GET' or 'PUT' for the HTTP method", r.Method), http.StatusBadRequest) 74 | return 75 | } 76 | }) 77 | } 78 | 79 | func main() {} 80 | -------------------------------------------------------------------------------- /apps/redis-sample/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "spin-redis-sample" 5 | version = "0.1.0" 6 | authors = ["Fermyon Engineering Team "] 7 | 8 | [variables] 9 | redis_endpoint = {required = true} 10 | 11 | [[trigger.http]] 12 | route = "/..." 13 | component = "spin-redis-sample" 14 | 15 | [component.spin-redis-sample] 16 | source = "main.wasm" 17 | allowed_outbound_hosts = ["redis://*"] 18 | 19 | [component.spin-redis-sample.build] 20 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 21 | watch = ["**/*.go", "go.mod"] 22 | 23 | [component.spin-redis-sample.variables] 24 | redis_endpoint = "{{ redis_endpoint }}" 25 | -------------------------------------------------------------------------------- /apps/salutations/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/salutations/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/salutations 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.2.0 6 | 7 | require github.com/julienschmidt/httprouter v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /apps/salutations/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.2.0 h1:zHZdIqjbUwyxiwdygHItnM+vUUNSZ3CX43jbIUemBI4= 2 | github.com/fermyon/spin/sdk/go/v2 v2.2.0/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /apps/salutations/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | spinhttp "github.com/fermyon/spin/sdk/go/v2/http" 8 | ) 9 | 10 | func init() { 11 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 12 | w.Header().Set("Content-Type", "text/plain") 13 | fmt.Fprintln(w, "Goodbye!") 14 | }) 15 | } 16 | 17 | func main() {} 18 | -------------------------------------------------------------------------------- /apps/salutations/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "salutations" 5 | version = "0.1.0" 6 | authors = ["Kate Goldenring "] 7 | description = "An app that gives salutations" 8 | 9 | [[trigger.http]] 10 | route = "/hi" 11 | component = "hello" 12 | 13 | [component.hello] 14 | source = "../hello-world/main.wasm" 15 | allowed_outbound_hosts = [] 16 | [component.hello.build] 17 | command = "cd ../hello-world && tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 18 | watch = ["**/*.go", "go.mod"] 19 | 20 | [[trigger.http]] 21 | route = "/bye" 22 | component = "goodbye" 23 | 24 | [component.goodbye] 25 | source = "main.wasm" 26 | allowed_outbound_hosts = [] 27 | [component.goodbye.build] 28 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 29 | watch = ["**/*.go", "go.mod"] -------------------------------------------------------------------------------- /apps/variable-explorer/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/variable-explorer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "variable-explorer" 3 | authors = ["Thorsten Hans "] 4 | description = "" 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | spin-sdk = "2.2.0" 14 | 15 | [workspace] 16 | -------------------------------------------------------------------------------- /apps/variable-explorer/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "variable-explorer" 5 | version = "0.1.0" 6 | authors = ["Thorsten Hans "] 7 | description = "" 8 | 9 | [variables] 10 | log_level = { default = "WARN" } 11 | platform_name = { default = "Fermyon Cloud" } 12 | db_password = { required = true } 13 | 14 | [[trigger.http]] 15 | route = "/..." 16 | component = "variable-explorer" 17 | 18 | [component.variable-explorer] 19 | source = "target/wasm32-wasi/release/variable_explorer.wasm" 20 | allowed_outbound_hosts = [] 21 | 22 | [component.variable-explorer.variables] 23 | log_level = "{{ log_level }}" 24 | platform_name = "{{ platform_name }}" 25 | db_password = "{{ db_password }}" 26 | 27 | [component.variable-explorer.build] 28 | command = "cargo build --target wasm32-wasi --release" 29 | watch = ["src/**/*.rs", "Cargo.toml"] 30 | -------------------------------------------------------------------------------- /apps/variable-explorer/src/lib.rs: -------------------------------------------------------------------------------- 1 | use spin_sdk::http::{IntoResponse, Request, Response}; 2 | use spin_sdk::{http_component, variables}; 3 | 4 | /// A simple Spin HTTP component. 5 | #[http_component] 6 | fn handle_variable_explorer(_req: Request) -> anyhow::Result { 7 | let log_level = variables::get("log_level")?; 8 | let platform_name = variables::get("platform_name")?; 9 | let db_password = variables::get("db_password")?; 10 | 11 | println!("# Log Level: {}", log_level); 12 | println!("# Platform name: {}", platform_name); 13 | println!("# DB Password: {}", db_password); 14 | 15 | Ok(Response::builder() 16 | .status(200) 17 | .header("content-type", "text/plain") 18 | .body(format!("Hello from {}", platform_name)) 19 | .build()) 20 | } 21 | -------------------------------------------------------------------------------- /apps/variabletester/.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .spin/ 3 | -------------------------------------------------------------------------------- /apps/variabletester/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} tinygo/tinygo:0.30.0 AS build 2 | WORKDIR /opt/build 3 | 4 | COPY go.mod go.sum . 5 | 6 | # cache deps before building and copying source so that we don't need to re-download as much 7 | # and so that source changes don't invalidate our downloaded layer 8 | RUN go mod download 9 | 10 | COPY . . 11 | 12 | RUN tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go 13 | 14 | FROM scratch 15 | COPY --from=build /opt/build/main.wasm . 16 | COPY --from=build /opt/build/spin.toml . 17 | -------------------------------------------------------------------------------- /apps/variabletester/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/variabletester 2 | 3 | go 1.20 4 | 5 | require github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021 6 | 7 | require github.com/julienschmidt/httprouter v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /apps/variabletester/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021 h1:btVvhHStqDFm7or/+CKIwcJbOYLy8Co/U/qs2D6DYM8= 2 | github.com/fermyon/spin/sdk/go/v2 v2.0.0-20240122224952-c973a878f021/go.mod h1:kfJ+gdf/xIaKrsC6JHCUDYMv2Bzib1ohFIYUzvP+SCw= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /apps/variabletester/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | spinhttp "github.com/fermyon/spin/sdk/go/v2/http" 8 | "github.com/fermyon/spin/sdk/go/v2/variables" 9 | ) 10 | 11 | func init() { 12 | spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Content-Type", "text/plain") 14 | greetee, err := variables.Get("greetee") 15 | if err != nil { 16 | fmt.Fprintf(w, "err: %s\n", err) 17 | return 18 | } 19 | 20 | fmt.Fprintf(w, "Hello %s!\n", greetee) 21 | }) 22 | } 23 | 24 | func main() {} 25 | -------------------------------------------------------------------------------- /apps/variabletester/spin.toml: -------------------------------------------------------------------------------- 1 | spin_manifest_version = 2 2 | 3 | [application] 4 | name = "variabletester" 5 | version = "0.1.0" 6 | authors = ["Danielle Lancashire "] 7 | description = "" 8 | 9 | [[trigger.http]] 10 | route = "/..." 11 | component = "variabletester" 12 | 13 | [variables] 14 | greetee = { required = true } 15 | 16 | [component.variabletester] 17 | source = "main.wasm" 18 | allowed_outbound_hosts = [] 19 | 20 | [component.variabletester.variables] 21 | greetee = "{{ greetee }}" 22 | 23 | [component.variabletester.build] 24 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" 25 | watch = ["**/*.go", "go.mod"] 26 | -------------------------------------------------------------------------------- /charts/spin-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/spin-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: spin-operator 3 | description: A Helm chart for Kubernetes 4 | # A chart can be either an 'application' or a 'library' chart. 5 | # 6 | # Application charts are a collection of templates that can be packaged into versioned archives 7 | # to be deployed. 8 | # 9 | # Library charts provide useful utilities or functions for the chart developer. They're included as 10 | # a dependency of application charts to inject those utilities and functions into the rendering 11 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 12 | type: application 13 | # This is the chart version. This version number should be incremented each time you make changes 14 | # to the chart and its templates, including the app version. 15 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 16 | # NOTE: this version is kept static in version control but is bumped when packaging and releasing 17 | version: 0.5.0 18 | # This is the version number of the application being deployed. This version number should be 19 | # incremented each time you make changes to the application. Versions are not expected to 20 | # follow Semantic Versioning. They should reflect the version the application is using. 21 | # It is recommended to use it with quotes. 22 | # NOTE: this version is kept static in version control but is bumped when packaging and releasing 23 | appVersion: "v0.5.0" 24 | -------------------------------------------------------------------------------- /charts/spin-operator/README.md: -------------------------------------------------------------------------------- 1 | # spin-operator 2 | 3 | spin-operator is a Kubernetes operator in charge of handling the lifecycle of Spin applications based on their SpinApp resources. 4 | 5 | ## Prerequisites 6 | 7 | - Kubernetes v1.11.3+ 8 | 9 | ## Prepare the cluster 10 | 11 | Prior to installing the chart, you'll need to ensure the following: 12 | 13 | - [Cert Manager](https://github.com/cert-manager/cert-manager) to automatically provision and manage TLS certificates (used by spin-operator's admission webhook system). Cert Manager must be running and the corresponding CRDs must be present on the cluster before installing the spin-operator chart. 14 | 15 | - [Kwasm Operator](https://github.com/kwasm/kwasm-operator) to install WebAssembly support on Kubernetes nodes. See the [project README.md](https://github.com/KWasm/kwasm-operator/blob/main/README.md) for installation and configuration steps, including annotating nodes to run Spin/wasm workloads. 16 | 17 | - spin-operator CustomResourceDefinition (CRD) resources are installed. This includes the SpinApp CRD representing Spin applications to be scheduled on the cluster. 18 | 19 | ```console 20 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinapps.yaml 21 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinappexecutors.yaml 22 | ``` 23 | 24 | ## Installing the chart 25 | 26 | The following installs the chart with the release name `spin-operator`: 27 | 28 | ```console 29 | $ helm install spin-operator \ 30 | --namespace spin-operator \ 31 | --create-namespace \ 32 | --version {{ CHART_VERSION }} \ 33 | oci://ghcr.io/spinframework/charts/spin-operator 34 | ``` 35 | 36 | ## Post-installation 37 | 38 | spin-operator depends on the following resources. If not already present on the cluster, install them now: 39 | 40 | - An application executor is installed. This is the executor that spin-operator uses to run Spin applications. 41 | 42 | ```console 43 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/samples/spin-shim-executor.yaml 44 | ``` 45 | 46 | - A RuntimeClass resource for the `wasmtime-spin-v2` container runtime is installed. This is the runtime that Spin applications use. 47 | 48 | ```console 49 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/samples/spin-runtime-class.yaml 50 | ``` 51 | 52 | ## Upgrading the chart 53 | 54 | Note that you may also need to upgrade the spin-operator CRDs in tandem with upgrading the Helm release: 55 | 56 | ```console 57 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinapps.yaml 58 | $ kubectl apply -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinappexecutors.yaml 59 | ``` 60 | 61 | To upgrade the `spin-operator` release, run the following: 62 | 63 | ```console 64 | $ helm upgrade spin-operator \ 65 | --namespace spin-operator \ 66 | --version {{ CHART_VERSION }} \ 67 | oci://ghcr.io/spinframework/charts/spin-operator 68 | ``` 69 | 70 | ## Uninstalling the chart 71 | 72 | To delete the `spin-operator` release, run: 73 | 74 | ```console 75 | $ helm delete spin-operator --namespace spin-operator 76 | ``` 77 | 78 | This will remove all Kubernetes resources associated with the chart and deletes the Helm release. 79 | 80 | To completely uninstall all resources related to spin-operator, you may want to delete the corresponding CRD resources and, optionally, the RuntimeClass: 81 | 82 | ```console 83 | $ kubectl delete -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/samples/spin-runtime-class.yaml 84 | $ kubectl delete -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/samples/spin-shim-executor.yaml 85 | $ kubectl delete -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinapps.yaml 86 | $ kubectl delete -f https://raw.githubusercontent.com/spinframework/spin-operator/main/config/crd/bases/core.spinkube.dev_spinappexecutors.yaml 87 | ``` 88 | -------------------------------------------------------------------------------- /charts/spin-operator/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | spin-operator {{ .Chart.Version }} is now deployed! 2 | 3 | Your release is named {{ .Release.Name }}. 4 | 5 | To learn more about the release, try: 6 | 7 | $ helm --namespace {{ .Release.Namespace }} status {{ .Release.Name }} 8 | $ helm --namespace {{ .Release.Namespace }} get all {{ .Release.Name }} 9 | 10 | Note: spin-operator requires a few additional resources to be present on the 11 | Kubernetes cluster before it can run the first Spin application. If you haven't 12 | already done so, please ensure the following: 13 | 14 | 1. Install the containerd-shim-spin SpinAppExecutor: 15 | 16 | $ kubectl apply -f https://github.com/spinframework/spin-operator/releases/download/v{{ .Chart.Version }}/spin-operator.shim-executor.yaml 17 | 18 | 2. Install the wasmtime-spin-v2 RuntimeClass: 19 | 20 | $ kubectl apply -f https://github.com/spinframework/spin-operator/releases/download/v{{ .Chart.Version }}/spin-operator.runtime-class.yaml 21 | 22 | You are now ready to deploy your first Spin app! 23 | 24 | For further details, see this chart's README: 25 | 26 | $ helm show readme oci://ghcr.io/spinframework/charts/spin-operator 27 | -------------------------------------------------------------------------------- /charts/spin-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "spin-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "spin-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | 27 | {{/* 28 | helmify replaces namespace name with `{{ .Release.Namespace }}` in dnsNames for Certificate object 29 | which means `{{ include "spin-operator.fullname" . }}` gets replaced with `{{ include "{{ .Release.Namespace }}.fullname" . }}` 30 | 31 | This is most likely a bug in helmify, but we can workaround it by defining a new template helper with name `{{ .Release.Namespace }}.fullname` 32 | */}} 33 | {{- define "{{ .Release.Namespace }}.fullname" -}} 34 | {{ include "spin-operator.fullname" . }} 35 | {{- end }} 36 | 37 | {{/* 38 | Create chart name and version as used by the chart label. 39 | */}} 40 | {{- define "spin-operator.chart" -}} 41 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 42 | {{- end }} 43 | 44 | {{/* 45 | Common labels 46 | */}} 47 | {{- define "spin-operator.labels" -}} 48 | helm.sh/chart: {{ include "spin-operator.chart" . }} 49 | {{ include "spin-operator.selectorLabels" . }} 50 | {{- if .Chart.AppVersion }} 51 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 52 | {{- end }} 53 | app.kubernetes.io/managed-by: {{ .Release.Service }} 54 | {{- end }} 55 | 56 | {{/* 57 | Selector labels 58 | */}} 59 | {{- define "spin-operator.selectorLabels" -}} 60 | app.kubernetes.io/name: {{ include "spin-operator.name" . }} 61 | app.kubernetes.io/instance: {{ .Release.Name }} 62 | {{- end }} 63 | 64 | {{/* 65 | Create the name of the service account to use 66 | */}} 67 | {{- define "spin-operator.serviceAccountName" -}} 68 | {{- if .Values.serviceAccount.create }} 69 | {{- default (include "spin-operator.fullname" .) .Values.serviceAccount.name }} 70 | {{- else }} 71 | {{- default "default" .Values.serviceAccount.name }} 72 | {{- end }} 73 | {{- end }} 74 | -------------------------------------------------------------------------------- /charts/spin-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: manager 7 | app.kubernetes.io/created-by: spin-operator 8 | app.kubernetes.io/part-of: spin-operator 9 | control-plane: controller-manager 10 | {{- include "spin-operator.labels" . | nindent 4 }} 11 | spec: 12 | replicas: {{ .Values.controllerManager.replicas }} 13 | selector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | {{- include "spin-operator.selectorLabels" . | nindent 6 }} 17 | template: 18 | metadata: 19 | labels: 20 | control-plane: controller-manager 21 | {{- include "spin-operator.selectorLabels" . | nindent 8 }} 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | spec: 25 | affinity: 26 | nodeAffinity: 27 | requiredDuringSchedulingIgnoredDuringExecution: 28 | nodeSelectorTerms: 29 | - matchExpressions: 30 | - key: kubernetes.io/arch 31 | operator: In 32 | values: 33 | - amd64 34 | - arm64 35 | - key: kubernetes.io/os 36 | operator: In 37 | values: 38 | - linux 39 | containers: 40 | - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} 41 | command: 42 | - /manager 43 | env: 44 | - name: KUBERNETES_CLUSTER_DOMAIN 45 | value: {{ quote .Values.kubernetesClusterDomain }} 46 | image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag 47 | | default .Chart.AppVersion }} 48 | imagePullPolicy: {{ .Values.controllerManager.manager.imagePullPolicy }} 49 | livenessProbe: 50 | httpGet: 51 | path: /healthz 52 | port: 8082 53 | initialDelaySeconds: 15 54 | periodSeconds: 20 55 | name: manager 56 | ports: 57 | - containerPort: 9443 58 | name: webhook-server 59 | protocol: TCP 60 | readinessProbe: 61 | httpGet: 62 | path: /readyz 63 | port: 8082 64 | initialDelaySeconds: 5 65 | periodSeconds: 10 66 | resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 67 | }} 68 | securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext 69 | | nindent 10 }} 70 | volumeMounts: 71 | - mountPath: /tmp/k8s-webhook-server/serving-certs 72 | name: cert 73 | readOnly: true 74 | securityContext: 75 | runAsNonRoot: true 76 | serviceAccountName: {{ include "spin-operator.fullname" . }}-controller-manager 77 | terminationGracePeriodSeconds: 10 78 | volumes: 79 | - name: cert 80 | secret: 81 | defaultMode: 420 82 | secretName: webhook-server-cert -------------------------------------------------------------------------------- /charts/spin-operator/templates/leader-election-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-leader-election-role 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: spin-operator 8 | app.kubernetes.io/part-of: spin-operator 9 | {{- include "spin-operator.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - coordination.k8s.io 25 | resources: 26 | - leases 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - create 32 | - update 33 | - patch 34 | - delete 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - events 39 | verbs: 40 | - create 41 | - patch 42 | --- 43 | apiVersion: rbac.authorization.k8s.io/v1 44 | kind: RoleBinding 45 | metadata: 46 | name: {{ include "spin-operator.fullname" . }}-leader-election-rolebinding 47 | labels: 48 | app.kubernetes.io/component: rbac 49 | app.kubernetes.io/created-by: spin-operator 50 | app.kubernetes.io/part-of: spin-operator 51 | {{- include "spin-operator.labels" . | nindent 4 }} 52 | roleRef: 53 | apiGroup: rbac.authorization.k8s.io 54 | kind: Role 55 | name: '{{ include "spin-operator.fullname" . }}-leader-election-role' 56 | subjects: 57 | - kind: ServiceAccount 58 | name: '{{ include "spin-operator.fullname" . }}-controller-manager' 59 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/spin-operator/templates/manager-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-manager-role 5 | labels: 6 | {{- include "spin-operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - events 12 | verbs: 13 | - create 14 | - patch 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - secrets 19 | - services 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - apps 30 | resources: 31 | - deployments 32 | verbs: 33 | - create 34 | - delete 35 | - get 36 | - list 37 | - patch 38 | - update 39 | - watch 40 | - apiGroups: 41 | - apps 42 | resources: 43 | - deployments/status 44 | verbs: 45 | - get 46 | - apiGroups: 47 | - core.spinkube.dev 48 | resources: 49 | - spinappexecutors 50 | - spinapps 51 | verbs: 52 | - create 53 | - delete 54 | - get 55 | - list 56 | - patch 57 | - update 58 | - watch 59 | - apiGroups: 60 | - core.spinkube.dev 61 | resources: 62 | - spinappexecutors/finalizers 63 | verbs: 64 | - update 65 | - apiGroups: 66 | - core.spinkube.dev 67 | resources: 68 | - spinappexecutors/status 69 | - spinapps/status 70 | verbs: 71 | - get 72 | - patch 73 | - update 74 | --- 75 | apiVersion: rbac.authorization.k8s.io/v1 76 | kind: ClusterRoleBinding 77 | metadata: 78 | name: {{ include "spin-operator.fullname" . }}-manager-rolebinding 79 | labels: 80 | app.kubernetes.io/component: rbac 81 | app.kubernetes.io/created-by: spin-operator 82 | app.kubernetes.io/part-of: spin-operator 83 | {{- include "spin-operator.labels" . | nindent 4 }} 84 | roleRef: 85 | apiGroup: rbac.authorization.k8s.io 86 | kind: ClusterRole 87 | name: '{{ include "spin-operator.fullname" . }}-manager-role' 88 | subjects: 89 | - kind: ServiceAccount 90 | name: '{{ include "spin-operator.fullname" . }}-controller-manager' 91 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/spin-operator/templates/metrics-auth-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-metrics-auth-role 5 | labels: 6 | {{- include "spin-operator.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - authentication.k8s.io 10 | resources: 11 | - tokenreviews 12 | verbs: 13 | - create 14 | - apiGroups: 15 | - authorization.k8s.io 16 | resources: 17 | - subjectaccessreviews 18 | verbs: 19 | - create 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: ClusterRoleBinding 23 | metadata: 24 | name: {{ include "spin-operator.fullname" . }}-metrics-auth-rolebinding 25 | labels: 26 | {{- include "spin-operator.labels" . | nindent 4 }} 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: '{{ include "spin-operator.fullname" . }}-metrics-auth-role' 31 | subjects: 32 | - kind: ServiceAccount 33 | name: '{{ include "spin-operator.fullname" . }}-controller-manager' 34 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/spin-operator/templates/metrics-reader-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-metrics-reader 5 | labels: 6 | {{- include "spin-operator.labels" . | nindent 4 }} 7 | rules: 8 | - nonResourceURLs: 9 | - /metrics 10 | verbs: 11 | - get -------------------------------------------------------------------------------- /charts/spin-operator/templates/metrics-role-and-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-metrics-role 5 | labels: 6 | app.kubernetes.io/component: metrics-server 7 | app.kubernetes.io/created-by: spin-operator 8 | app.kubernetes.io/part-of: spin-operator 9 | {{- include "spin-operator.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - authentication.k8s.io 13 | resources: 14 | - tokenreviews 15 | verbs: 16 | - create 17 | - apiGroups: 18 | - authorization.k8s.io 19 | resources: 20 | - subjectaccessreviews 21 | verbs: 22 | - create 23 | --- 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | kind: ClusterRoleBinding 26 | metadata: 27 | name: {{ include "spin-operator.fullname" . }}-metrics-rolebinding 28 | labels: 29 | app.kubernetes.io/component: metrics-server 30 | app.kubernetes.io/created-by: spin-operator 31 | app.kubernetes.io/part-of: spin-operator 32 | {{- include "spin-operator.labels" . | nindent 4 }} 33 | roleRef: 34 | apiGroup: rbac.authorization.k8s.io 35 | kind: ClusterRole 36 | name: '{{ include "spin-operator.fullname" . }}-metrics-role' 37 | subjects: 38 | - kind: ServiceAccount 39 | name: '{{ include "spin-operator.fullname" . }}-controller-manager' 40 | namespace: '{{ .Release.Namespace }}' -------------------------------------------------------------------------------- /charts/spin-operator/templates/metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-metrics-service 5 | labels: 6 | control-plane: controller-manager 7 | {{- include "spin-operator.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.metricsService.type }} 10 | selector: 11 | control-plane: controller-manager 12 | {{- include "spin-operator.selectorLabels" . | nindent 4 }} 13 | ports: 14 | {{- .Values.metricsService.ports | toYaml | nindent 2 -}} -------------------------------------------------------------------------------- /charts/spin-operator/templates/mutating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-mutating-webhook-configuration 5 | annotations: 6 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "spin-operator.fullname" . }}-serving-cert 7 | labels: 8 | {{- include "spin-operator.labels" . | nindent 4 }} 9 | webhooks: 10 | - admissionReviewVersions: 11 | - v1 12 | clientConfig: 13 | service: 14 | name: '{{ include "spin-operator.fullname" . }}-webhook-service' 15 | namespace: '{{ .Release.Namespace }}' 16 | path: /mutate-core-spinkube-dev-v1alpha1-spinapp 17 | failurePolicy: Fail 18 | name: mspinapp.kb.io 19 | rules: 20 | - apiGroups: 21 | - core.spinkube.dev 22 | apiVersions: 23 | - v1alpha1 24 | operations: 25 | - CREATE 26 | - UPDATE 27 | resources: 28 | - spinapps 29 | sideEffects: None 30 | - admissionReviewVersions: 31 | - v1 32 | clientConfig: 33 | service: 34 | name: '{{ include "spin-operator.fullname" . }}-webhook-service' 35 | namespace: '{{ .Release.Namespace }}' 36 | path: /mutate-core-spinkube-dev-v1alpha1-spinappexecutor 37 | failurePolicy: Fail 38 | name: mspinappexecutor.kb.io 39 | rules: 40 | - apiGroups: 41 | - core.spinkube.dev 42 | apiVersions: 43 | - v1alpha1 44 | operations: 45 | - CREATE 46 | - UPDATE 47 | resources: 48 | - spinappexecutors 49 | sideEffects: None -------------------------------------------------------------------------------- /charts/spin-operator/templates/selfsigned-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Issuer 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-selfsigned-issuer 5 | labels: 6 | {{- include "spin-operator.labels" . | nindent 4 }} 7 | spec: 8 | selfSigned: {} -------------------------------------------------------------------------------- /charts/spin-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-controller-manager 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: spin-operator 8 | app.kubernetes.io/part-of: spin-operator 9 | {{- include "spin-operator.labels" . | nindent 4 }} 10 | annotations: 11 | {{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }} -------------------------------------------------------------------------------- /charts/spin-operator/templates/serving-cert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-serving-cert 5 | labels: 6 | {{- include "spin-operator.labels" . | nindent 4 }} 7 | spec: 8 | dnsNames: 9 | - '{{ include "{{ .Release.Namespace }}.fullname" . }}-webhook-service.{{ .Release.Namespace 10 | }}.svc' 11 | - '{{ include "{{ .Release.Namespace }}.fullname" . }}-webhook-service.{{ .Release.Namespace 12 | }}.svc.{{ .Values.kubernetesClusterDomain }}' 13 | issuerRef: 14 | kind: Issuer 15 | name: '{{ include "spin-operator.fullname" . }}-selfsigned-issuer' 16 | secretName: webhook-server-cert -------------------------------------------------------------------------------- /charts/spin-operator/templates/validating-webhook-configuration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-validating-webhook-configuration 5 | annotations: 6 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "spin-operator.fullname" . }}-serving-cert 7 | labels: 8 | {{- include "spin-operator.labels" . | nindent 4 }} 9 | webhooks: 10 | - admissionReviewVersions: 11 | - v1 12 | clientConfig: 13 | service: 14 | name: '{{ include "spin-operator.fullname" . }}-webhook-service' 15 | namespace: '{{ .Release.Namespace }}' 16 | path: /validate-core-spinkube-dev-v1alpha1-spinapp 17 | failurePolicy: Fail 18 | name: vspinapp.kb.io 19 | rules: 20 | - apiGroups: 21 | - core.spinkube.dev 22 | apiVersions: 23 | - v1alpha1 24 | operations: 25 | - CREATE 26 | - UPDATE 27 | resources: 28 | - spinapps 29 | sideEffects: None 30 | - admissionReviewVersions: 31 | - v1 32 | clientConfig: 33 | service: 34 | name: '{{ include "spin-operator.fullname" . }}-webhook-service' 35 | namespace: '{{ .Release.Namespace }}' 36 | path: /validate-core-spinkube-dev-v1alpha1-spinappexecutor 37 | failurePolicy: Fail 38 | name: vspinappexecutor.kb.io 39 | rules: 40 | - apiGroups: 41 | - core.spinkube.dev 42 | apiVersions: 43 | - v1alpha1 44 | operations: 45 | - CREATE 46 | - UPDATE 47 | resources: 48 | - spinappexecutors 49 | sideEffects: None -------------------------------------------------------------------------------- /charts/spin-operator/templates/webhook-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "spin-operator.fullname" . }}-webhook-service 5 | labels: 6 | app.kubernetes.io/component: webhook 7 | app.kubernetes.io/created-by: spin-operator 8 | app.kubernetes.io/part-of: spin-operator 9 | {{- include "spin-operator.labels" . | nindent 4 }} 10 | spec: 11 | type: {{ .Values.webhookService.type }} 12 | selector: 13 | control-plane: controller-manager 14 | {{- include "spin-operator.selectorLabels" . | nindent 4 }} 15 | ports: 16 | {{- .Values.webhookService.ports | toYaml | nindent 2 -}} -------------------------------------------------------------------------------- /charts/spin-operator/values.yaml: -------------------------------------------------------------------------------- 1 | ## Spin Operator configuration 2 | 3 | ## controllerManager represents the Spin Operator deployment. 4 | controllerManager: 5 | ## manager represents the Spin Operator container. 6 | manager: 7 | ## args are the default arguments to supply to the operator. 8 | ## In general, these should be left as-is. 9 | args: 10 | - --health-probe-bind-address=:8082 11 | - --metrics-bind-address=:8443 12 | - --leader-elect 13 | - --enable-webhooks 14 | ## containerSecurityContext defines privilege and access control for the 15 | ## container. 16 | ## See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 17 | containerSecurityContext: 18 | allowPrivilegeEscalation: false 19 | capabilities: 20 | drop: 21 | - ALL 22 | ## image indicates which repository and tag combination will be used for 23 | ## pulling the operator image. 24 | image: 25 | repository: ghcr.io/spinframework/spin-operator 26 | ## By default, .Chart.AppVersion is used as the tag. 27 | ## Updating this value to a version not aligned with the current chart 28 | ## version may lead to unexpected or broken behavior. 29 | # tag: latest 30 | imagePullPolicy: IfNotPresent 31 | ## resources represent default cpu/mem limits for the operator container. 32 | resources: 33 | # TODO: update these per https://github.com/spinframework/spin-operator/issues/21 34 | limits: 35 | cpu: 500m 36 | memory: 128Mi 37 | requests: 38 | cpu: 10m 39 | memory: 64Mi 40 | 41 | # replicas represent how many pod replicas of the controllerManager to run. 42 | replicas: 1 43 | 44 | # serviceAccount represents configuration for the controllerManager Service Account. 45 | # See https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ 46 | serviceAccount: 47 | annotations: {} 48 | 49 | ## kubernetesClusterDomain represents the domain used for service DNS within the cluster. 50 | kubernetesClusterDomain: cluster.local 51 | 52 | ## metricsService configuration. 53 | ## This configuration should only be updated in tandem with corresponding 54 | ## controller configuration. 55 | metricsService: 56 | ports: 57 | - name: https 58 | port: 8443 59 | protocol: TCP 60 | targetPort: 8443 61 | type: ClusterIP 62 | 63 | ## webhookService configuration. 64 | webhookService: 65 | ports: 66 | - port: 443 67 | protocol: TCP 68 | targetPort: 9443 69 | type: ClusterIP 70 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: certificate 9 | app.kubernetes.io/instance: serving-cert 10 | app.kubernetes.io/component: certificate 11 | app.kubernetes.io/created-by: spin-operator 12 | app.kubernetes.io/part-of: spin-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: selfsigned-issuer 15 | namespace: system 16 | spec: 17 | selfSigned: {} 18 | --- 19 | apiVersion: cert-manager.io/v1 20 | kind: Certificate 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: certificate 24 | app.kubernetes.io/instance: serving-cert 25 | app.kubernetes.io/component: certificate 26 | app.kubernetes.io/created-by: spin-operator 27 | app.kubernetes.io/part-of: spin-operator 28 | app.kubernetes.io/managed-by: kustomize 29 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 30 | namespace: system 31 | spec: 32 | # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize 33 | dnsNames: 34 | - SERVICE_NAME.SERVICE_NAMESPACE.svc 35 | - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local 36 | issuerRef: 37 | kind: Issuer 38 | name: selfsigned-issuer 39 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 40 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | -------------------------------------------------------------------------------- /config/chart/README.md: -------------------------------------------------------------------------------- 1 | This chart directory currently holds files related to the 2 | [Spin Operator Helm chart](../../charts/spin-operator/) that we inject after 3 | [helmify](../../README.md#packaging-and-deployment-via-helm) performs chart 4 | (re-)generation. 5 | 6 | As an example, helmify produces an auto-generated 7 | `values.yaml` but we'd like to provide a more ergonomic and descriptive 8 | version for users, eg with ample comments. 9 | 10 | If we no longer use helmify, this configuration directory can be removed. 11 | -------------------------------------------------------------------------------- /config/chart/values.yaml: -------------------------------------------------------------------------------- 1 | ## Spin Operator configuration 2 | 3 | ## controllerManager represents the Spin Operator deployment. 4 | controllerManager: 5 | ## manager represents the Spin Operator container. 6 | manager: 7 | ## args are the default arguments to supply to the operator. 8 | ## In general, these should be left as-is. 9 | args: 10 | - --health-probe-bind-address=:8082 11 | - --metrics-bind-address=:8443 12 | - --leader-elect 13 | - --enable-webhooks 14 | ## containerSecurityContext defines privilege and access control for the 15 | ## container. 16 | ## See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 17 | containerSecurityContext: 18 | allowPrivilegeEscalation: false 19 | capabilities: 20 | drop: 21 | - ALL 22 | ## image indicates which repository and tag combination will be used for 23 | ## pulling the operator image. 24 | image: 25 | repository: ghcr.io/spinkube/spin-operator 26 | ## By default, .Chart.AppVersion is used as the tag. 27 | ## Updating this value to a version not aligned with the current chart 28 | ## version may lead to unexpected or broken behavior. 29 | # tag: latest 30 | imagePullPolicy: IfNotPresent 31 | ## resources represent default cpu/mem limits for the operator container. 32 | resources: 33 | # TODO: update these per https://github.com/spinkube/spin-operator/issues/21 34 | limits: 35 | cpu: 500m 36 | memory: 128Mi 37 | requests: 38 | cpu: 10m 39 | memory: 64Mi 40 | 41 | # replicas represent how many pod replicas of the controllerManager to run. 42 | replicas: 1 43 | 44 | # serviceAccount represents configuration for the controllerManager Service Account. 45 | # See https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ 46 | serviceAccount: 47 | annotations: {} 48 | 49 | ## kubernetesClusterDomain represents the domain used for service DNS within the cluster. 50 | kubernetesClusterDomain: cluster.local 51 | 52 | ## metricsService configuration. 53 | ## This configuration should only be updated in tandem with corresponding 54 | ## controller configuration. 55 | metricsService: 56 | ports: 57 | - name: https 58 | port: 8443 59 | protocol: TCP 60 | targetPort: 8443 61 | type: ClusterIP 62 | 63 | ## webhookService configuration. 64 | webhookService: 65 | ports: 66 | - port: 443 67 | protocol: TCP 68 | targetPort: 9443 69 | type: ClusterIP 70 | -------------------------------------------------------------------------------- /config/crd/bases/core.spinkube.dev_spinappexecutors.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.16.5 7 | name: spinappexecutors.core.spinkube.dev 8 | spec: 9 | group: core.spinkube.dev 10 | names: 11 | kind: SpinAppExecutor 12 | listKind: SpinAppExecutorList 13 | plural: spinappexecutors 14 | singular: spinappexecutor 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: SpinAppExecutor is the Schema for the spinappexecutors API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: SpinAppExecutorSpec defines the desired state of SpinAppExecutor 41 | properties: 42 | createDeployment: 43 | description: |- 44 | CreateDeployment specifies whether the Executor wants the SpinKube operator 45 | to create a deployment for the application or if it will be realized externally. 46 | type: boolean 47 | deploymentConfig: 48 | description: |- 49 | DeploymentConfig specifies how the deployment should be configured when 50 | createDeployment is true. 51 | properties: 52 | caCertSecret: 53 | description: |- 54 | CACertSecret specifies the name of the secret containing the CA 55 | certificates to be mounted to the deployment. 56 | type: string 57 | installDefaultCACerts: 58 | description: |- 59 | InstallDefaultCACerts specifies whether the default CA 60 | certificate bundle should be generated. When set a new secret 61 | will be created containing the certificates. If no secret name is 62 | defined in `CACertSecret` the secret name will be `spin-ca`. 63 | type: boolean 64 | otel: 65 | description: Otel provides Kubernetes Bindings to Otel Variables. 66 | properties: 67 | exporter_otlp_endpoint: 68 | description: ExporterOtlpEndpoint configures the default combined 69 | otlp endpoint for sending telemetry 70 | type: string 71 | exporter_otlp_logs_endpoint: 72 | description: ExporterOtlpLogsEndpoint configures the logs-specific 73 | otlp endpoint 74 | type: string 75 | exporter_otlp_metrics_endpoint: 76 | description: ExporterOtlpMetricsEndpoint configures the metrics-specific 77 | otlp endpoint 78 | type: string 79 | exporter_otlp_traces_endpoint: 80 | description: ExporterOtlpTracesEndpoint configures the trace-specific 81 | otlp endpoint 82 | type: string 83 | type: object 84 | runtimeClassName: 85 | description: |- 86 | RuntimeClassName is the runtime class name that should be used by pods created 87 | as part of a deployment. This should only be defined when SpintainerImage is not defined. 88 | type: string 89 | spinImage: 90 | description: |- 91 | SpinImage points to an image that will run Spin in a container to execute 92 | your SpinApp. This is an alternative to using the shim to execute your 93 | SpinApp. This should only be defined when RuntimeClassName is not 94 | defined. When specified, application images must be available without 95 | authentication. 96 | type: string 97 | type: object 98 | required: 99 | - createDeployment 100 | type: object 101 | status: 102 | description: SpinAppExecutorStatus defines the observed state of SpinAppExecutor 103 | type: object 104 | type: object 105 | served: true 106 | storage: true 107 | subresources: 108 | status: {} 109 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/core.spinkube.dev_spinapps.yaml 6 | - bases/core.spinkube.dev_spinappexecutors.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patches: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | - path: patches/webhook_in_spinapps.yaml 13 | - path: patches/webhook_in_spinappexecutors.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | - path: patches/cainjection_in_spinapps.yaml 19 | - path: patches/cainjection_in_spinappexecutors.yaml 20 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # [WEBHOOK] To enable webhook, uncomment the following section 23 | # the following config is for teaching kustomize how to do kustomization for CRDs. 24 | 25 | configurations: 26 | - kustomizeconfig.yaml 27 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_spinappexecutors.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: spinappexecutors.core.spinkube.dev 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_spinapps.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: spinapps.core.spinkube.dev 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_spinappexecutors.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: spinappexecutors.core.spinkube.dev 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_spinapps.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: spinapps.core.spinkube.dev 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/managed-by: kustomize 7 | name: metrics-service 8 | namespace: system 9 | spec: 10 | ports: 11 | - name: https 12 | port: 8443 13 | protocol: TCP 14 | targetPort: 8443 15 | selector: 16 | control-plane: controller-manager -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: mutatingwebhookconfiguration 8 | app.kubernetes.io/instance: mutating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: spin-operator 11 | app.kubernetes.io/part-of: spin-operator 12 | app.kubernetes.io/managed-by: kustomize 13 | name: mutating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 16 | --- 17 | apiVersion: admissionregistration.k8s.io/v1 18 | kind: ValidatingWebhookConfiguration 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: validatingwebhookconfiguration 22 | app.kubernetes.io/instance: validating-webhook-configuration 23 | app.kubernetes.io/component: webhook 24 | app.kubernetes.io/created-by: spin-operator 25 | app.kubernetes.io/part-of: spin-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | name: validating-webhook-configuration 28 | annotations: 29 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 30 | -------------------------------------------------------------------------------- /config/manager/kustomization.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: spin-operator 25 | app.kubernetes.io/part-of: spin-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | affinity: 40 | nodeAffinity: 41 | requiredDuringSchedulingIgnoredDuringExecution: 42 | nodeSelectorTerms: 43 | - matchExpressions: 44 | - key: kubernetes.io/arch 45 | operator: In 46 | values: 47 | - amd64 48 | - arm64 49 | - key: kubernetes.io/os 50 | operator: In 51 | values: 52 | - linux 53 | securityContext: 54 | runAsNonRoot: true 55 | # TODO(user): For common cases that do not require escalating privileges 56 | # it is recommended to ensure that all your Pods/Containers are restrictive. 57 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 58 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 59 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 60 | # seccompProfile: 61 | # type: RuntimeDefault 62 | containers: 63 | - command: 64 | - /manager 65 | args: 66 | - --leader-elect 67 | - --enable-webhooks 68 | image: ghcr.io/spinkube/spin-operator:latest 69 | name: manager 70 | imagePullPolicy: IfNotPresent 71 | securityContext: 72 | allowPrivilegeEscalation: false 73 | capabilities: 74 | drop: 75 | - "ALL" 76 | livenessProbe: 77 | httpGet: 78 | path: /healthz 79 | port: 8082 80 | initialDelaySeconds: 15 81 | periodSeconds: 20 82 | readinessProbe: 83 | httpGet: 84 | path: /readyz 85 | port: 8082 86 | initialDelaySeconds: 5 87 | periodSeconds: 10 88 | # TODO(user): Configure the resources accordingly based on the project requirements. 89 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 90 | resources: 91 | limits: 92 | cpu: 500m 93 | memory: 128Mi 94 | requests: 95 | cpu: 10m 96 | memory: 64Mi 97 | serviceAccountName: controller-manager 98 | terminationGracePeriodSeconds: 10 99 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: servicemonitor 8 | app.kubernetes.io/instance: controller-manager-metrics-monitor 9 | app.kubernetes.io/component: metrics 10 | app.kubernetes.io/created-by: spin-operator 11 | app.kubernetes.io/part-of: spin-operator 12 | app.kubernetes.io/managed-by: kustomize 13 | name: controller-manager-metrics-monitor 14 | namespace: system 15 | spec: 16 | endpoints: 17 | - path: /metrics 18 | port: https 19 | scheme: https 20 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 21 | tlsConfig: 22 | insecureSkipVerify: true 23 | selector: 24 | matchLabels: 25 | control-plane: controller-manager 26 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # The following RBAC configurations are used to protect 13 | # the metrics endpoint with authn/authz. These configurations 14 | # ensure that only authorized users and service accounts 15 | # can access the metrics endpoint. Comment the following 16 | # permissions if you want to disable this protection. 17 | # More info: https://book.kubebuilder.io/reference/metrics.html 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: spin-operator 9 | app.kubernetes.io/part-of: spin-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - secrets 18 | - services 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - apps 29 | resources: 30 | - deployments 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | - apiGroups: 40 | - apps 41 | resources: 42 | - deployments/status 43 | verbs: 44 | - get 45 | - apiGroups: 46 | - core.spinkube.dev 47 | resources: 48 | - spinappexecutors 49 | - spinapps 50 | verbs: 51 | - create 52 | - delete 53 | - get 54 | - list 55 | - patch 56 | - update 57 | - watch 58 | - apiGroups: 59 | - core.spinkube.dev 60 | resources: 61 | - spinappexecutors/finalizers 62 | verbs: 63 | - update 64 | - apiGroups: 65 | - core.spinkube.dev 66 | resources: 67 | - spinappexecutors/status 68 | - spinapps/status 69 | verbs: 70 | - get 71 | - patch 72 | - update 73 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: spin-operator 9 | app.kubernetes.io/part-of: spin-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: spin-operator 9 | app.kubernetes.io/part-of: spin-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/spinapp_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit spinapps. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: spinapp-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: spinapp-editor-role 13 | rules: 14 | - apiGroups: 15 | - core.spinkube.dev 16 | resources: 17 | - spinapps 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - core.spinkube.dev 28 | resources: 29 | - spinapps/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/spinapp_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view spinapps. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: spinapp-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: spinapp-viewer-role 13 | rules: 14 | - apiGroups: 15 | - core.spinkube.dev 16 | resources: 17 | - spinapps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - core.spinkube.dev 24 | resources: 25 | - spinapps/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/spinappexecutor_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit spinappexecutors. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: spinappexecutor-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: spinappexecutor-editor-role 13 | rules: 14 | - apiGroups: 15 | - core.spinkube.dev 16 | resources: 17 | - spinappexecutors 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - core.spinkube.dev 28 | resources: 29 | - spinappexecutors/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/spinappexecutor_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view spinappexecutors. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: spinappexecutor-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: spin-operator 10 | app.kubernetes.io/part-of: spin-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: spinappexecutor-viewer-role 13 | rules: 14 | - apiGroups: 15 | - core.spinkube.dev 16 | resources: 17 | - spinappexecutors 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - core.spinkube.dev 24 | resources: 25 | - spinappexecutors/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/samples/annotations.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: annotations-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | serviceAnnotations: 10 | key: value 11 | deploymentAnnotations: 12 | key: value 13 | multiple-keys: are-supported 14 | podAnnotations: 15 | key: value 16 | -------------------------------------------------------------------------------- /config/samples/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: hpa-spinapp 5 | spec: 6 | image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986 7 | executor: containerd-shim-spin 8 | enableAutoscaling: true 9 | resources: 10 | limits: 11 | cpu: 500m 12 | memory: 500Mi 13 | requests: 14 | cpu: 100m 15 | memory: 400Mi 16 | --- 17 | apiVersion: autoscaling/v2 18 | kind: HorizontalPodAutoscaler 19 | metadata: 20 | name: spinapp-autoscaler 21 | spec: 22 | scaleTargetRef: 23 | apiVersion: apps/v1 24 | kind: Deployment 25 | name: hpa-spinapp 26 | minReplicas: 1 27 | maxReplicas: 10 28 | metrics: 29 | - type: Resource 30 | resource: 31 | name: cpu 32 | target: 33 | type: Utilization 34 | averageUtilization: 50 35 | -------------------------------------------------------------------------------- /config/samples/keda-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: keda-spinapp 5 | spec: 6 | image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986 7 | executor: containerd-shim-spin 8 | enableAutoscaling: true 9 | resources: 10 | limits: 11 | cpu: 500m 12 | memory: 500Mi 13 | requests: 14 | cpu: 100m 15 | memory: 400Mi 16 | -------------------------------------------------------------------------------- /config/samples/keda-scaledobject.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: keda.sh/v1alpha1 2 | kind: ScaledObject 3 | metadata: 4 | name: cpu-scaling 5 | spec: 6 | scaleTargetRef: 7 | name: keda-spinapp 8 | minReplicaCount: 1 9 | maxReplicaCount: 20 10 | triggers: 11 | - type: cpu 12 | metricType: Utilization 13 | metadata: 14 | value: "50" 15 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - annotations.yaml 4 | - hpa.yaml 5 | - private-image.yaml 6 | - probes.yaml 7 | - redis.yaml 8 | - resources.yaml 9 | - runtime-config.yaml 10 | - spin-shim-executor.yaml 11 | - simple.yaml 12 | - variables.yaml 13 | - volume-mount.yaml 14 | #+kubebuilder:scaffold:manifestskustomizesamples 15 | -------------------------------------------------------------------------------- /config/samples/otel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: otel-spinapp 5 | spec: 6 | image: ghcr.io/spinkube/spin-operator/cpu-load-gen:20240311-163328-g1121986 7 | executor: otel-shim-executor 8 | replicas: 1 9 | --- 10 | apiVersion: core.spinkube.dev/v1alpha1 11 | kind: SpinAppExecutor 12 | metadata: 13 | name: otel-shim-executor 14 | spec: 15 | createDeployment: true 16 | deploymentConfig: 17 | runtimeClassName: wasmtime-spin-v2 18 | installDefaultCACerts: true 19 | otel: 20 | exporter_otlp_endpoint: http://otel-collector.default.svc.cluster.local:4318 21 | -------------------------------------------------------------------------------- /config/samples/private-image.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: private-image-spinapp 5 | spec: 6 | image: "ghcr.io//:" 7 | # For testing, you can create a secret with the following command: 8 | # 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 9 | imagePullSecrets: 10 | - name: spin-image-secret 11 | replicas: 1 12 | executor: containerd-shim-spin -------------------------------------------------------------------------------- /config/samples/probes.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: healthchecks-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | checks: 10 | liveness: 11 | httpGet: 12 | path: "/hello" 13 | readiness: 14 | httpGet: 15 | path: "/go-hello" 16 | -------------------------------------------------------------------------------- /config/samples/redis.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: redis-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/spin-operator/redis-sample:20240820-095510-g8d6b442" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | # Steps to run this found at https://github.com/spinkube/spin-operator/pull/131 -------------------------------------------------------------------------------- /config/samples/resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: resource-requirements-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | resources: 10 | limits: 11 | cpu: 500m 12 | memory: 500Mi 13 | requests: 14 | cpu: 100m 15 | memory: 400Mi 16 | -------------------------------------------------------------------------------- /config/samples/runtime-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: runtime-config 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | runtimeConfig: 10 | sqliteDatabases: 11 | - name: "default" 12 | type: "libsql" 13 | options: 14 | - name: "url" 15 | value: "https://sensational-penguin-ahacker.libsql.example.com" 16 | - name: "token" 17 | valueFrom: 18 | secretKeyRef: 19 | name: "my-super-secret" 20 | key: "turso-token" 21 | 22 | llmCompute: 23 | type: "remote_http" 24 | options: 25 | - name: "url" 26 | value: "https://llm-app.fermyon.app" 27 | - name: "auth_token" 28 | valueFrom: 29 | secretKeyRef: 30 | name: "my-super-secret" 31 | key: "llm-token" 32 | 33 | keyValueStores: 34 | - name: "default" 35 | type: "redis" 36 | options: 37 | - name: "url" 38 | valueFrom: 39 | secretKeyRef: 40 | name: "my-super-secret" 41 | key: "redis-full-url" 42 | 43 | -------------------------------------------------------------------------------- /config/samples/selective-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: hello-salutation-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/spin-operator/salutations:20241105-223428-g4da3171" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | # Configure the application to only contain the "hello" component 10 | # Who doesn't hate goodbyes? 11 | components: ["hello"] 12 | -------------------------------------------------------------------------------- /config/samples/simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: simple-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | -------------------------------------------------------------------------------- /config/samples/spin-runtime-class.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: node.k8s.io/v1 2 | kind: RuntimeClass 3 | metadata: 4 | name: wasmtime-spin-v2 5 | handler: spin 6 | -------------------------------------------------------------------------------- /config/samples/spin-shim-executor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinAppExecutor 3 | metadata: 4 | name: containerd-shim-spin 5 | spec: 6 | createDeployment: true 7 | deploymentConfig: 8 | runtimeClassName: wasmtime-spin-v2 9 | installDefaultCACerts: true 10 | -------------------------------------------------------------------------------- /config/samples/spintainer-executor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinAppExecutor 3 | metadata: 4 | name: spintainer 5 | spec: 6 | createDeployment: true 7 | deploymentConfig: 8 | installDefaultCACerts: true 9 | spinImage: ghcr.io/fermyon/spin:v2.7.0 10 | -------------------------------------------------------------------------------- /config/samples/spintainer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: spintainer-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/spin-operator/hello-world:20240708-130250-gfefd2b1" 7 | replicas: 1 8 | executor: spintainer 9 | -------------------------------------------------------------------------------- /config/samples/variable-explorer.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | metadata: 4 | name: spin-app-cfg 5 | data: 6 | logLevel: INFO 7 | --- 8 | kind: Secret 9 | apiVersion: v1 10 | metadata: 11 | name: spin-app-secret 12 | data: 13 | password: c2VjcmV0X3NhdWNlCg== 14 | --- 15 | kind: SpinApp 16 | apiVersion: core.spinkube.dev/v1alpha1 17 | metadata: 18 | name: variable-explorer 19 | spec: 20 | replicas: 1 21 | image: ttl.sh/variable-explorer:1h 22 | executor: containerd-shim-spin 23 | variables: 24 | - name: platform_name 25 | value: Kubernetes 26 | - name: log_level 27 | valueFrom: 28 | configMapKeyRef: 29 | name: spin-app-cfg 30 | key: logLevel 31 | optional: true 32 | - name: db_password 33 | valueFrom: 34 | secretKeyRef: 35 | name: spin-app-secret 36 | key: password 37 | optional: false -------------------------------------------------------------------------------- /config/samples/variables.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: variables-spinapp 5 | spec: 6 | image: "ghcr.io/endocrimes/spin-variabletester:container" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | variables: 10 | - name: greetee 11 | value: Fermyon 12 | -------------------------------------------------------------------------------- /config/samples/volume-mount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: volume-mount-spinapp 5 | spec: 6 | image: "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 7 | replicas: 1 8 | executor: containerd-shim-spin 9 | volumes: 10 | - name: example-volume 11 | persistentVolumeClaim: 12 | claimName: example-pv-claim 13 | volumeMounts: 14 | - name: example-volume 15 | mountPath: "/mnt/data" 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolumeClaim 19 | metadata: 20 | name: example-pv-claim 21 | spec: 22 | storageClassName: manual 23 | accessModes: 24 | - ReadWriteOnce 25 | resources: 26 | requests: 27 | storage: 100Mi 28 | --- 29 | apiVersion: v1 30 | kind: PersistentVolume 31 | metadata: 32 | name: pv-volume 33 | labels: 34 | type: local 35 | spec: 36 | storageClassName: manual 37 | capacity: 38 | storage: 100Mi 39 | accessModes: 40 | - ReadWriteOnce 41 | hostPath: 42 | path: "/mnt/data" -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting nameReference. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-core-spinkube-dev-v1alpha1-spinapp 14 | failurePolicy: Fail 15 | name: mspinapp.kb.io 16 | rules: 17 | - apiGroups: 18 | - core.spinkube.dev 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - spinapps 26 | sideEffects: None 27 | - admissionReviewVersions: 28 | - v1 29 | clientConfig: 30 | service: 31 | name: webhook-service 32 | namespace: system 33 | path: /mutate-core-spinkube-dev-v1alpha1-spinappexecutor 34 | failurePolicy: Fail 35 | name: mspinappexecutor.kb.io 36 | rules: 37 | - apiGroups: 38 | - core.spinkube.dev 39 | apiVersions: 40 | - v1alpha1 41 | operations: 42 | - CREATE 43 | - UPDATE 44 | resources: 45 | - spinappexecutors 46 | sideEffects: None 47 | --- 48 | apiVersion: admissionregistration.k8s.io/v1 49 | kind: ValidatingWebhookConfiguration 50 | metadata: 51 | name: validating-webhook-configuration 52 | webhooks: 53 | - admissionReviewVersions: 54 | - v1 55 | clientConfig: 56 | service: 57 | name: webhook-service 58 | namespace: system 59 | path: /validate-core-spinkube-dev-v1alpha1-spinapp 60 | failurePolicy: Fail 61 | name: vspinapp.kb.io 62 | rules: 63 | - apiGroups: 64 | - core.spinkube.dev 65 | apiVersions: 66 | - v1alpha1 67 | operations: 68 | - CREATE 69 | - UPDATE 70 | resources: 71 | - spinapps 72 | sideEffects: None 73 | - admissionReviewVersions: 74 | - v1 75 | clientConfig: 76 | service: 77 | name: webhook-service 78 | namespace: system 79 | path: /validate-core-spinkube-dev-v1alpha1-spinappexecutor 80 | failurePolicy: Fail 81 | name: vspinappexecutor.kb.io 82 | rules: 83 | - apiGroups: 84 | - core.spinkube.dev 85 | apiVersions: 86 | - v1alpha1 87 | operations: 88 | - CREATE 89 | - UPDATE 90 | resources: 91 | - spinappexecutors 92 | sideEffects: None 93 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: service 6 | app.kubernetes.io/instance: webhook-service 7 | app.kubernetes.io/component: webhook 8 | app.kubernetes.io/created-by: spin-operator 9 | app.kubernetes.io/part-of: spin-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: webhook-service 12 | namespace: system 13 | spec: 14 | ports: 15 | - port: 443 16 | protocol: TCP 17 | targetPort: 9443 18 | selector: 19 | control-plane: controller-manager 20 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2e Testing 2 | 3 | This e2e test suite leverages the [Kubernetes e2e-framework](https://github.com/kubernetes-sigs/e2e-framework) project for writing and running e2e tests for the spin operator project. 4 | 5 | To run tests: 6 | 7 | ```console 8 | go test ./e2e -v 9 | ``` 10 | -------------------------------------------------------------------------------- /e2e/crd_installed_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | apiextensionsV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | "sigs.k8s.io/e2e-framework/pkg/envconf" 9 | "sigs.k8s.io/e2e-framework/pkg/features" 10 | ) 11 | 12 | func TestCRDInstalled(t *testing.T) { 13 | crdInstalledFeature := features.New("crd installed"). 14 | Assess("spinapp crd installed", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 15 | client := cfg.Client() 16 | if err := apiextensionsV1.AddToScheme(client.Resources().GetScheme()); err != nil { 17 | t.Fatalf("failed to register the v1 API extension types with Kubernetes scheme: %s", err) 18 | } 19 | name := "spinapps.core.spinkube.dev" 20 | var crd apiextensionsV1.CustomResourceDefinition 21 | if err := client.Resources().Get(ctx, name, "", &crd); err != nil { 22 | t.Fatalf("SpinApp CRD not found: %s", err) 23 | } 24 | 25 | if crd.Spec.Group != "core.spinkube.dev" { 26 | t.Fatalf("SpinApp CRD has unexpected group: %s", crd.Spec.Group) 27 | } 28 | return ctx 29 | 30 | }). 31 | Assess("spinappexecutor crd installed", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 32 | client := cfg.Client() 33 | if err := apiextensionsV1.AddToScheme(client.Resources().GetScheme()); err != nil { 34 | t.Fatalf("failed to register the v1 API extension types with Kubernetes scheme: %s", err) 35 | } 36 | 37 | name := "spinappexecutors.core.spinkube.dev" 38 | var crd apiextensionsV1.CustomResourceDefinition 39 | if err := client.Resources().Get(ctx, name, "", &crd); err != nil { 40 | t.Fatalf("SpinApp CRD not found: %s", err) 41 | } 42 | 43 | if crd.Spec.Group != "core.spinkube.dev" { 44 | t.Fatalf("SpinAppExecutor CRD has unexpected group: %s", crd.Spec.Group) 45 | } 46 | return ctx 47 | }).Feature() 48 | testEnv.Test(t, crdInstalledFeature) 49 | } 50 | -------------------------------------------------------------------------------- /e2e/default_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "sigs.k8s.io/e2e-framework/klient" 11 | "sigs.k8s.io/e2e-framework/klient/wait" 12 | "sigs.k8s.io/e2e-framework/klient/wait/conditions" 13 | "sigs.k8s.io/e2e-framework/pkg/envconf" 14 | "sigs.k8s.io/e2e-framework/pkg/features" 15 | 16 | spinapps_v1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 17 | ) 18 | 19 | var runtimeClassName = "wasmtime-spin-v2" 20 | 21 | // TestDefaultSetup is a test that checks that the minimal setup works 22 | // with the containerd wasm shim runtime as the default runtime. 23 | func TestDefaultSetup(t *testing.T) { 24 | var client klient.Client 25 | 26 | helloWorldImage := "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0" 27 | testSpinAppName := "test-spinapp" 28 | 29 | defaultTest := features.New("default and most minimal setup"). 30 | Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 31 | client = cfg.Client() 32 | 33 | testSpinApp := newSpinAppCR(testSpinAppName, helloWorldImage, "containerd-shim-spin", nil) 34 | if err := client.Resources().Create(ctx, testSpinApp); err != nil { 35 | t.Fatalf("Failed to create spinapp: %s", err) 36 | } 37 | 38 | return ctx 39 | }). 40 | Assess("spin app deployment is created and available", 41 | func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 42 | if err := wait.For( 43 | conditions.New(client.Resources()).DeploymentAvailable(testSpinAppName, testNamespace), 44 | wait.WithTimeout(3*time.Minute), 45 | wait.WithInterval(time.Second), 46 | ); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | return ctx 51 | }). 52 | Assess("spin app service is created and available", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 53 | svc := &corev1.ServiceList{ 54 | Items: []corev1.Service{ 55 | {ObjectMeta: metav1.ObjectMeta{Name: testSpinAppName, Namespace: testNamespace}}, 56 | }, 57 | } 58 | 59 | if err := wait.For( 60 | conditions.New(client.Resources()).ResourcesFound(svc), 61 | wait.WithTimeout(3*time.Minute), 62 | wait.WithInterval(time.Second), 63 | ); err != nil { 64 | t.Fatal(err) 65 | } 66 | return ctx 67 | }). 68 | Feature() 69 | testEnv.Test(t, defaultTest) 70 | } 71 | 72 | func newSpinAppCR(name, image, executor string, components []string) *spinapps_v1alpha1.SpinApp { 73 | app := spinapps_v1alpha1.SpinApp{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | Name: name, 76 | Namespace: testNamespace, 77 | }, 78 | Spec: spinapps_v1alpha1.SpinAppSpec{ 79 | Replicas: 1, 80 | Image: image, 81 | Executor: executor, 82 | }, 83 | } 84 | if components != nil { 85 | app.Spec.Components = components 86 | } 87 | return &app 88 | } 89 | -------------------------------------------------------------------------------- /e2e/helper/helper.go: -------------------------------------------------------------------------------- 1 | // helper is a package that offers e2e test helpers. It's a badly named package. 2 | // If it grows we should refactor it into something a little more manageable 3 | // but it's fine for now. 4 | package helper 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | appsv1 "k8s.io/api/apps/v1" 16 | corev1 "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | controllerruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 19 | "sigs.k8s.io/e2e-framework/klient/k8s/resources" 20 | "sigs.k8s.io/e2e-framework/klient/wait" 21 | "sigs.k8s.io/e2e-framework/klient/wait/conditions" 22 | "sigs.k8s.io/e2e-framework/pkg/envconf" 23 | ) 24 | 25 | const debugDeploymentName = "debugy" 26 | 27 | // EnsureDebugContainer ensures that the helper debug container is installed right namespace. This allows us to make requests from inside the cluster 28 | // regardless of the external network configuration. 29 | func EnsureDebugContainer(t *testing.T, ctx context.Context, cfg *envconf.Config, namespace string) { 30 | t.Helper() 31 | 32 | client, err := cfg.NewClient() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | // Deploy a debug container so that we can test that the app is available later 38 | deployment := newDebugDeployment(namespace, debugDeploymentName, 1, debugDeploymentName) 39 | if err = client.Resources().Create(ctx, deployment); controllerruntimeclient.IgnoreAlreadyExists(err) != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | err = wait.For( 44 | conditions.New(client.Resources()). 45 | DeploymentConditionMatch(deployment, appsv1.DeploymentAvailable, corev1.ConditionTrue), 46 | wait.WithTimeout(time.Minute*5)) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | 52 | // CurlSpinApp is a crude function for using the debug pod to send a HTTP request to a spin app 53 | // within the cluster. It allows customization of the route, and all requests are GET requests unless a non-empty body is provided. 54 | func CurlSpinApp(t *testing.T, ctx context.Context, cfg *envconf.Config, namespace, spinAppName, route, body string) (string, int, error) { 55 | t.Helper() 56 | 57 | client, err := cfg.NewClient() 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | // Find the debug pod 63 | pods := &corev1.PodList{} 64 | err = client.Resources(namespace).List(ctx, pods, resources.WithLabelSelector("app=pod-exec")) 65 | if err != nil || pods.Items == nil || len(pods.Items) == 0 { 66 | return "", -1, fmt.Errorf("failed to get debug pods: %w", err) 67 | } 68 | 69 | debugPod := pods.Items[0] 70 | 71 | podName := debugPod.Name 72 | 73 | command := []string{"curl", "--silent", "--max-time", "5", "--write-out", "\n%{http_code}\n", "http://" + spinAppName + "." + namespace + route, "--output", "-"} 74 | if body != "" { 75 | command = append(command, "--data", body) 76 | } 77 | 78 | var stdout, stderr bytes.Buffer 79 | if err := client.Resources().ExecInPod(ctx, namespace, podName, debugDeploymentName, command, &stdout, &stderr); err != nil { 80 | t.Logf("Curl Spin App failed, err: %v.\nstdout:\n%s\n\nstderr:\n%s\n", err, stdout.String(), stderr.String()) 81 | return "", -1, err 82 | } 83 | 84 | parts := strings.SplitN(stdout.String(), "\n", 2) 85 | if len(parts) != 2 { 86 | t.Fatalf("Curl Spin App failed, unexpected response format: %s", &stdout) 87 | } 88 | 89 | strStatus := strings.Trim(parts[1], "\n") 90 | statusCode, err := strconv.Atoi(strStatus) 91 | if err != nil { 92 | t.Logf("error parsing status code: %v", err) 93 | return parts[0], statusCode, err 94 | } 95 | t.Logf("Curl Spin App response: %s, status code: %d, err: %v", parts[0], statusCode, err) 96 | return parts[0], statusCode, nil 97 | 98 | } 99 | 100 | func newDebugDeployment(namespace string, name string, replicas int32, containerName string) *appsv1.Deployment { 101 | labels := map[string]string{"app": "pod-exec"} 102 | return &appsv1.Deployment{ 103 | ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, 104 | Spec: appsv1.DeploymentSpec{ 105 | Replicas: &replicas, 106 | Selector: &metav1.LabelSelector{ 107 | MatchLabels: labels, 108 | }, 109 | Template: corev1.PodTemplateSpec{ 110 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 111 | Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: containerName, Image: "nginx"}}}, 112 | }, 113 | }, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /e2e/k3d_provider.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "k8s.io/client-go/rest" 12 | "k8s.io/klog/v2" 13 | "sigs.k8s.io/e2e-framework/klient/conf" 14 | "sigs.k8s.io/e2e-framework/pkg/utils" 15 | ) 16 | 17 | const k3dImage = "ghcr.io/spinkube/containerd-shim-spin/k3d:20241015-215845-g71c8351" 18 | 19 | var k3dBin = "k3d" 20 | 21 | type Cluster struct { 22 | name string 23 | kubecfgFile string 24 | restConfig *rest.Config 25 | } 26 | 27 | func (c *Cluster) Create(context.Context, string) (string, error) { 28 | if err := findOrInstallK3d(); err != nil { 29 | return "", fmt.Errorf("k3d: failed to find or install k3d: %w", err) 30 | } 31 | 32 | if _, ok := clusterExists(c.name); ok { 33 | klog.V(4).Info("Skipping k3d Cluster creation. Cluster already created ", c.name) 34 | return c.GetKubeconfig() 35 | } 36 | 37 | command := fmt.Sprintf("%s cluster create %s --image %s -p '8081:80@loadbalancer' --agents 2 --wait", k3dBin, c.name, k3dImage) 38 | if useNativeSnapshotter() { 39 | command = command + ` --k3s-arg "--snapshotter=native@agent:0,1;server:0"` 40 | } 41 | klog.V(4).Info("Launching:", command) 42 | p := utils.RunCommand(command) 43 | if p.Err() != nil { 44 | outBytes, err := io.ReadAll(p.Out()) 45 | if err != nil { 46 | klog.ErrorS(err, "failed to read data from the k3d create process output due to an error") 47 | } 48 | return "", fmt.Errorf("k3d: failed to create cluster %q: %w: %s: %s", c.name, p.Err(), p.Result(), string(outBytes)) 49 | } 50 | 51 | clusters, ok := clusterExists(c.name) 52 | if !ok { 53 | return "", fmt.Errorf("k3d Cluster.Create: cluster %v still not in 'cluster list' after creation: %v", c.name, clusters) 54 | } 55 | klog.V(4).Info("k3d cluster available: ", clusters) 56 | 57 | kConfig, err := c.GetKubeconfig() 58 | if err != nil { 59 | return "", err 60 | } 61 | return kConfig, c.initKubernetesAccessClients() 62 | } 63 | 64 | func (c *Cluster) Destroy() error { 65 | p := utils.RunCommand(fmt.Sprintf(`%s cluster delete %s`, k3dBin, c.name)) 66 | if p.Err() != nil { 67 | return fmt.Errorf("%s cluster delete: %w", k3dBin, p.Err()) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (c *Cluster) GetKubeconfig() (string, error) { 74 | kubecfg := fmt.Sprintf("%s-kubecfg", c.name) 75 | 76 | p := utils.RunCommand(fmt.Sprintf(`%s kubeconfig get %s`, k3dBin, c.name)) 77 | 78 | if p.Err() != nil { 79 | return "", fmt.Errorf("k3d get kubeconfig: %w", p.Err()) 80 | } 81 | 82 | var stdout bytes.Buffer 83 | if _, err := stdout.ReadFrom(p.Out()); err != nil { 84 | return "", fmt.Errorf("k3d kubeconfig stdout bytes: %w", err) 85 | } 86 | 87 | file, err := os.CreateTemp("", fmt.Sprintf("k3d-cluster-%s", kubecfg)) 88 | if err != nil { 89 | return "", fmt.Errorf("k3d kubeconfig file: %w", err) 90 | } 91 | defer file.Close() 92 | 93 | c.kubecfgFile = file.Name() 94 | 95 | if n, err := io.Copy(file, &stdout); n == 0 || err != nil { 96 | return "", fmt.Errorf("k3d kubecfg file: bytes copied: %d: %w]", n, err) 97 | } 98 | 99 | return file.Name(), nil 100 | } 101 | 102 | func (c *Cluster) initKubernetesAccessClients() error { 103 | cfg, err := conf.New(c.kubecfgFile) 104 | if err != nil { 105 | return err 106 | } 107 | c.restConfig = cfg 108 | 109 | return nil 110 | } 111 | 112 | func findOrInstallK3d() error { 113 | _, err := utils.FindOrInstallGoBasedProvider(k3dBin, k3dBin, "github.com/k3d-io/k3d/v5", "v5.6.3") 114 | return err 115 | } 116 | 117 | func clusterExists(name string) (string, bool) { 118 | clusters := utils.FetchCommandOutput(fmt.Sprintf("%s cluster list --no-headers", k3dBin)) 119 | for _, c := range strings.Split(clusters, "\n") { 120 | cl := strings.Split(c, " ")[0] 121 | if cl == name { 122 | return clusters, true 123 | } 124 | } 125 | return clusters, false 126 | } 127 | 128 | func useNativeSnapshotter() bool { 129 | return os.Getenv("E2E_USE_NATIVE_SNAPSHOTTER") == "true" // nolint:forbidigo 130 | } 131 | -------------------------------------------------------------------------------- /e2e/main_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | nodev1 "k8s.io/api/node/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "sigs.k8s.io/e2e-framework/klient/wait" 13 | "sigs.k8s.io/e2e-framework/klient/wait/conditions" 14 | "sigs.k8s.io/e2e-framework/pkg/env" 15 | "sigs.k8s.io/e2e-framework/pkg/envconf" 16 | "sigs.k8s.io/e2e-framework/pkg/envfuncs" 17 | "sigs.k8s.io/e2e-framework/pkg/utils" 18 | 19 | spinapps_v1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 20 | ) 21 | 22 | const ErrFormat = "%v: %v\n" 23 | 24 | var ( 25 | testEnv env.Environment 26 | testNamespace string 27 | testCACertSecret = "test-spin-ca" 28 | spinOperatorDeploymentName = "spin-operator-controller-manager" 29 | spinOperatorNamespace = "spin-operator" 30 | cluster = &Cluster{} 31 | ) 32 | 33 | func TestMain(m *testing.M) { 34 | cfg, _ := envconf.NewFromFlags() 35 | testEnv = env.NewWithConfig(cfg) 36 | testNamespace = envconf.RandomName("my-ns", 10) 37 | cluster.name = envconf.RandomName("crdtest-", 16) 38 | 39 | testEnv.Setup( 40 | func(ctx context.Context, e *envconf.Config) (context.Context, error) { 41 | if _, err := cluster.Create(ctx, cluster.name); err != nil { 42 | return ctx, err 43 | } 44 | e.WithKubeconfigFile(cluster.kubecfgFile) 45 | 46 | return ctx, nil 47 | }, 48 | 49 | envfuncs.CreateNamespace(testNamespace), 50 | 51 | // build and load spin operator image into cluster 52 | func(ctx context.Context, _ *envconf.Config) (context.Context, error) { 53 | if os.Getenv("E2E_SKIP_BUILD") == "" { // nolint:forbidigo 54 | if p := utils.RunCommand(`bash -c "cd .. && IMG=ghcr.io/spinkube/spin-operator:dev make docker-build"`); p.Err() != nil { 55 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 56 | } 57 | } 58 | 59 | if p := utils.RunCommand(("k3d image import -c " + cluster.name + " ghcr.io/spinkube/spin-operator:dev")); p.Err() != nil { 60 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 61 | } 62 | return ctx, nil 63 | }, 64 | 65 | // install spin operator and pre-reqs 66 | func(ctx context.Context, _ *envconf.Config) (context.Context, error) { 67 | // install crds 68 | if p := utils.RunCommand(`bash -c "cd .. && make install"`); p.Err() != nil { 69 | 70 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 71 | 72 | } 73 | 74 | // install cert-manager 75 | if p := utils.RunCommand("kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.2/cert-manager.yaml"); p.Err() != nil { 76 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 77 | } 78 | // wait for cert-manager to be ready 79 | if p := utils.RunCommand("kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager-webhook -n cert-manager"); p.Err() != nil { 80 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 81 | } 82 | 83 | if p := utils.RunCommand(`bash -c "cd .. && IMG=ghcr.io/spinkube/spin-operator:dev make deploy"`); p.Err() != nil { 84 | return ctx, fmt.Errorf(ErrFormat, p.Err(), p.Out()) 85 | } 86 | 87 | // wait for the controller deployment to be ready 88 | client := cfg.Client() 89 | 90 | if err := spinapps_v1alpha1.AddToScheme(client.Resources().GetScheme()); err != nil { 91 | return ctx, fmt.Errorf("failed to register the spinapps_v1alpha1 types with Kubernetes scheme: %w", err) 92 | } 93 | 94 | if err := wait.For( 95 | conditions.New(client.Resources()). 96 | DeploymentAvailable(spinOperatorDeploymentName, spinOperatorNamespace), 97 | wait.WithTimeout(3*time.Minute), 98 | wait.WithInterval(10*time.Second), 99 | ); err != nil { 100 | return ctx, err 101 | } 102 | 103 | return ctx, nil 104 | }, 105 | 106 | // create runtime class 107 | func(ctx context.Context, c *envconf.Config) (context.Context, error) { 108 | client := cfg.Client() 109 | runtimeClass := &nodev1.RuntimeClass{ 110 | ObjectMeta: metav1.ObjectMeta{ 111 | Name: runtimeClassName, 112 | }, 113 | Handler: "spin", 114 | } 115 | 116 | err := client.Resources().Create(ctx, runtimeClass) 117 | return ctx, err 118 | }, 119 | 120 | // create executor 121 | func(ctx context.Context, c *envconf.Config) (context.Context, error) { 122 | client := cfg.Client() 123 | 124 | err := client.Resources().Create(ctx, newContainerdShimExecutor(testNamespace)) 125 | return ctx, err 126 | }, 127 | ) 128 | 129 | testEnv.Finish( 130 | func(ctx context.Context, _ *envconf.Config) (context.Context, error) { 131 | return ctx, cluster.Destroy() 132 | }, 133 | ) 134 | 135 | os.Exit(testEnv.Run(m)) 136 | } 137 | 138 | func newContainerdShimExecutor(namespace string) *spinapps_v1alpha1.SpinAppExecutor { 139 | return &spinapps_v1alpha1.SpinAppExecutor{ 140 | ObjectMeta: metav1.ObjectMeta{ 141 | Name: "containerd-shim-spin", 142 | Namespace: namespace, 143 | }, 144 | Spec: spinapps_v1alpha1.SpinAppExecutorSpec{ 145 | CreateDeployment: true, 146 | DeploymentConfig: &spinapps_v1alpha1.ExecutorDeploymentConfig{ 147 | RuntimeClassName: &runtimeClassName, 148 | InstallDefaultCACerts: true, 149 | CACertSecret: testCACertSecret, 150 | }, 151 | }, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /e2e/spintainer_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | controllerruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/e2e-framework/klient" 12 | "sigs.k8s.io/e2e-framework/klient/wait" 13 | "sigs.k8s.io/e2e-framework/klient/wait/conditions" 14 | "sigs.k8s.io/e2e-framework/pkg/envconf" 15 | "sigs.k8s.io/e2e-framework/pkg/features" 16 | 17 | spinapps_v1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 18 | "github.com/spinkube/spin-operator/internal/generics" 19 | ) 20 | 21 | // TestSpintainer is a test that checks that the minimal setup works 22 | // with the spintainer executor 23 | func TestSpintainer(t *testing.T) { 24 | var client klient.Client 25 | 26 | helloWorldImage := "ghcr.io/spinkube/spin-operator/hello-world:20240708-130250-gfefd2b1" 27 | testSpinAppName := "test-spintainer-app" 28 | 29 | defaultTest := features.New("default and most minimal setup"). 30 | Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 31 | 32 | client = cfg.Client() 33 | 34 | if err := spinapps_v1alpha1.AddToScheme(client.Resources(testNamespace).GetScheme()); err != nil { 35 | t.Fatalf("failed to register the spinapps_v1alpha1 types with Kubernetes scheme: %s", err) 36 | } 37 | 38 | return ctx 39 | }). 40 | Assess("spin app custom resource is created", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 41 | testSpinApp := newSpinAppCR(testSpinAppName, helloWorldImage, "spintainer", nil) 42 | 43 | if err := client.Resources().Create(ctx, newSpintainerExecutor(testNamespace)); controllerruntimeclient.IgnoreAlreadyExists(err) != nil { 44 | t.Fatalf("Failed to create spinappexecutor: %s", err) 45 | } 46 | 47 | if err := client.Resources().Create(ctx, testSpinApp); err != nil { 48 | t.Fatalf("Failed to create spinapp: %s", err) 49 | } 50 | 51 | return ctx 52 | }). 53 | Assess("spin app deployment and service are available", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 54 | // wait for deployment to be ready 55 | if err := wait.For( 56 | conditions.New(client.Resources()).DeploymentAvailable(testSpinAppName, testNamespace), 57 | wait.WithTimeout(3*time.Minute), 58 | wait.WithInterval(time.Second), 59 | ); err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | svc := &v1.ServiceList{ 64 | Items: []v1.Service{ 65 | {ObjectMeta: metav1.ObjectMeta{Name: testSpinAppName, Namespace: testNamespace}}, 66 | }, 67 | } 68 | 69 | if err := wait.For( 70 | conditions.New(client.Resources()).ResourcesFound(svc), 71 | wait.WithTimeout(3*time.Minute), 72 | wait.WithInterval(500*time.Millisecond), 73 | ); err != nil { 74 | t.Fatal(err) 75 | } 76 | return ctx 77 | }). 78 | Feature() 79 | testEnv.Test(t, defaultTest) 80 | } 81 | 82 | func newSpintainerExecutor(namespace string) *spinapps_v1alpha1.SpinAppExecutor { 83 | var testSpinAppExecutor = &spinapps_v1alpha1.SpinAppExecutor{ 84 | ObjectMeta: metav1.ObjectMeta{ 85 | Name: "spintainer", 86 | Namespace: namespace, 87 | }, 88 | Spec: spinapps_v1alpha1.SpinAppExecutorSpec{ 89 | CreateDeployment: true, 90 | DeploymentConfig: &spinapps_v1alpha1.ExecutorDeploymentConfig{ 91 | // TODO: pin this to Spin 3.0 once released 92 | SpinImage: generics.Ptr("ghcr.io/fermyon/spin:canary"), 93 | }, 94 | }, 95 | } 96 | 97 | return testSpinAppExecutor 98 | } 99 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs-format", 7 | "nixpkgs" 8 | ], 9 | "rust-analyzer-src": "rust-analyzer-src" 10 | }, 11 | "locked": { 12 | "lastModified": 1637475807, 13 | "narHash": "sha256-E3nzOvlzZXwyo8Stp5upKsTCDcqUTYAFj4EC060A31c=", 14 | "owner": "nix-community", 15 | "repo": "fenix", 16 | "rev": "960e7fef45692a4fffc6df6d6b613b0399bbdfd5", 17 | "type": "github" 18 | }, 19 | "original": { 20 | "owner": "nix-community", 21 | "repo": "fenix", 22 | "type": "github" 23 | } 24 | }, 25 | "flake-utils": { 26 | "inputs": { 27 | "systems": "systems" 28 | }, 29 | "locked": { 30 | "lastModified": 1710146030, 31 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 35 | "type": "github" 36 | }, 37 | "original": { 38 | "owner": "numtide", 39 | "repo": "flake-utils", 40 | "type": "github" 41 | } 42 | }, 43 | "nixpkgs": { 44 | "locked": { 45 | "lastModified": 1723637854, 46 | "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", 47 | "owner": "NixOS", 48 | "repo": "nixpkgs", 49 | "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "id": "nixpkgs", 54 | "ref": "nixos-unstable", 55 | "type": "indirect" 56 | } 57 | }, 58 | "nixpkgs-format": { 59 | "inputs": { 60 | "fenix": "fenix", 61 | "flake-utils": [ 62 | "flake-utils" 63 | ], 64 | "nixpkgs": [ 65 | "nixpkgs" 66 | ] 67 | }, 68 | "locked": { 69 | "lastModified": 1721822211, 70 | "narHash": "sha256-zacOgNv3qM3AbSG3p5PT/Bfc4c7NoIqoLII8/jIUsOQ=", 71 | "owner": "nix-community", 72 | "repo": "nixpkgs-fmt", 73 | "rev": "bdb15b4c7e0cb49ae091dd43113d0a938afae02c", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "nix-community", 78 | "repo": "nixpkgs-fmt", 79 | "type": "github" 80 | } 81 | }, 82 | "root": { 83 | "inputs": { 84 | "flake-utils": "flake-utils", 85 | "nixpkgs": "nixpkgs", 86 | "nixpkgs-format": "nixpkgs-format" 87 | } 88 | }, 89 | "rust-analyzer-src": { 90 | "flake": false, 91 | "locked": { 92 | "lastModified": 1637439871, 93 | "narHash": "sha256-2awQ/obzl7zqYgLwbQL0zT58gN8Xq7n+81GcMiS595I=", 94 | "owner": "rust-analyzer", 95 | "repo": "rust-analyzer", 96 | "rev": "4566414789310acb2617543f4b50beab4bb48e06", 97 | "type": "github" 98 | }, 99 | "original": { 100 | "owner": "rust-analyzer", 101 | "ref": "nightly", 102 | "repo": "rust-analyzer", 103 | "type": "github" 104 | } 105 | }, 106 | "systems": { 107 | "locked": { 108 | "lastModified": 1681028828, 109 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 110 | "owner": "nix-systems", 111 | "repo": "default", 112 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 113 | "type": "github" 114 | }, 115 | "original": { 116 | "owner": "nix-systems", 117 | "repo": "default", 118 | "type": "github" 119 | } 120 | } 121 | }, 122 | "root": "root", 123 | "version": 7 124 | } 125 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "The Spin Operator for Kubernetes"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs/nixos-unstable"; 6 | 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | 9 | nixpkgs-format.url = "github:nix-community/nixpkgs-fmt"; 10 | nixpkgs-format.inputs.nixpkgs.follows = "nixpkgs"; 11 | nixpkgs-format.inputs.flake-utils.follows = "flake-utils"; 12 | }; 13 | 14 | outputs = { self, nixpkgs, flake-utils, nixpkgs-format }@inputs: 15 | flake-utils.lib.eachDefaultSystem (system: 16 | let 17 | pkgs = import nixpkgs { inherit system; }; 18 | 19 | buildDeps = with pkgs; [ 20 | go_1_23 21 | gnumake 22 | git 23 | ]; 24 | 25 | devDeps = with pkgs; buildDeps ++ [ 26 | gopls 27 | kubectl 28 | kubernetes-helm 29 | gotestsum 30 | golangci-lint 31 | ]; 32 | in 33 | { 34 | devShells.default = pkgs.mkShell { 35 | buildInputs = devDeps ++ [ 36 | nixpkgs-format.defaultPackage.${system} 37 | ]; 38 | }; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /format.Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to run prettier on markdown in the codebase. Used by lint-markdown 2 | # and lint-markdown-fix rules in Makefile. 3 | 4 | FROM node:alpine 5 | 6 | WORKDIR /usr/spin-operator 7 | 8 | RUN npm install prettier -g 9 | 10 | ENV PRETTIER_MODE=check 11 | 12 | CMD sh -c "npx prettier --${PRETTIER_MODE} '**/*.md'" 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spinkube/spin-operator 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/pelletier/go-toml/v2 v2.2.4 8 | github.com/prometheus/common v0.63.0 9 | github.com/stretchr/testify v1.10.0 10 | golang.org/x/sync v0.12.0 11 | k8s.io/api v0.32.3 12 | k8s.io/apiextensions-apiserver v0.32.3 13 | k8s.io/apimachinery v0.32.3 14 | k8s.io/client-go v0.32.3 15 | k8s.io/klog/v2 v2.130.1 16 | sigs.k8s.io/controller-runtime v0.20.3 17 | sigs.k8s.io/e2e-framework v0.6.0 18 | ) 19 | 20 | require ( 21 | cel.dev/expr v0.18.0 // indirect 22 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 23 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/blang/semver/v4 v4.0.0 // indirect 26 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 30 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 31 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/fsnotify/fsnotify v1.7.0 // indirect 34 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/go-logr/zapr v1.3.0 // indirect 37 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 38 | github.com/go-openapi/jsonreference v0.20.2 // indirect 39 | github.com/go-openapi/swag v0.23.0 // indirect 40 | github.com/gogo/protobuf v1.3.2 // indirect 41 | github.com/golang/protobuf v1.5.4 // indirect 42 | github.com/google/btree v1.1.3 // indirect 43 | github.com/google/cel-go v0.22.0 // indirect 44 | github.com/google/gnostic-models v0.6.8 // indirect 45 | github.com/google/go-cmp v0.7.0 // indirect 46 | github.com/google/gofuzz v1.2.0 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/gorilla/websocket v1.5.0 // indirect 49 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 50 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/klauspost/compress v1.17.9 // indirect 54 | github.com/mailru/easyjson v0.7.7 // indirect 55 | github.com/moby/spdystream v0.5.0 // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.2 // indirect 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 59 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 60 | github.com/pkg/errors v0.9.1 // indirect 61 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 62 | github.com/prometheus/client_golang v1.20.4 // indirect 63 | github.com/prometheus/client_model v0.6.1 // indirect 64 | github.com/prometheus/procfs v0.15.1 // indirect 65 | github.com/spf13/cobra v1.8.1 // indirect 66 | github.com/spf13/pflag v1.0.5 // indirect 67 | github.com/stoewer/go-strcase v1.3.0 // indirect 68 | github.com/vladimirvivien/gexe v0.4.1 // indirect 69 | github.com/x448/float16 v0.8.4 // indirect 70 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 71 | go.opentelemetry.io/otel v1.28.0 // indirect 72 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect 73 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect 74 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 75 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 76 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 77 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 78 | go.uber.org/multierr v1.11.0 // indirect 79 | go.uber.org/zap v1.27.0 // indirect 80 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 81 | golang.org/x/net v0.35.0 // indirect 82 | golang.org/x/oauth2 v0.25.0 // indirect 83 | golang.org/x/sys v0.30.0 // indirect 84 | golang.org/x/term v0.29.0 // indirect 85 | golang.org/x/text v0.22.0 // indirect 86 | golang.org/x/time v0.7.0 // indirect 87 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 88 | google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect 89 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect 90 | google.golang.org/grpc v1.65.0 // indirect 91 | google.golang.org/protobuf v1.36.5 // indirect 92 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 93 | gopkg.in/inf.v0 v0.9.1 // indirect 94 | gopkg.in/yaml.v3 v3.0.1 // indirect 95 | k8s.io/apiserver v0.32.3 // indirect 96 | k8s.io/component-base v0.32.3 // indirect 97 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 98 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 99 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect 100 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 101 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 102 | sigs.k8s.io/yaml v1.4.0 // indirect 103 | ) 104 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /hack/provision-minikube.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Provision a new minikube instance configured with the containerd shim for Spin. 5 | 6 | echo "Starting minikube" 7 | minikube start --container-runtime containerd 8 | 9 | echo "Installing the containerd shim" 10 | curl -fsSLO https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.10.0/containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz 11 | tar -zxvf containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz 12 | 13 | echo "Copying the shim to minikube" 14 | minikube cp containerd-shim-spin-v2 /usr/local/bin/ 15 | minikube ssh sudo chmod +x /usr/local/bin/containerd-shim-spin-v2 16 | 17 | # just cleaning up 18 | rm containerd-wasm-shims-v2-spin-linux-aarch64.tar.gz containerd-shim-spin-v2 19 | 20 | echo "Configuring containerd" 21 | if ! minikube ssh -- grep -q io.containerd.spin /etc/containerd/config.toml; then 22 | echo nope 23 | minikube ssh 'cat << EOF | sudo tee -a /etc/containerd/config.toml 24 | [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.spin] 25 | runtime_type = "io.containerd.spin.v2" 26 | EOF' 27 | fi 28 | 29 | echo "Restarting containerd" 30 | minikube ssh sudo systemctl restart containerd 31 | 32 | echo "Creating runtime class" 33 | kubectl apply -f - <"${secret_name}.yaml" 34 | apiVersion: v1 35 | kind: Secret 36 | metadata: 37 | name: $secret_name 38 | type: Opaque 39 | data: 40 | runtime-config.toml: $encoded_content 41 | EOF 42 | 43 | echo "Kubernetes secret created at ${secret_name}.yaml" -------------------------------------------------------------------------------- /internal/cacerts/cacerts.go: -------------------------------------------------------------------------------- 1 | // Package cacerts provides an embedded CA root certificates bundle. 2 | package cacerts 3 | 4 | // To update the default certificates run the following command in this 5 | // directory 6 | // 7 | // curl -sfL https://curl.se/ca/cacert.pem -o ca-certificates.crt 8 | 9 | import _ "embed" 10 | 11 | //go:embed ca-certificates.crt 12 | var caCertificates string 13 | 14 | // CACertificates returns the default bundle of CA root certificates. 15 | // The certificate bundle is under the MPL-2.0 licence from 16 | // https://curl.se/ca/cacert.pem. 17 | func CACertificates() string { 18 | return caCertificates 19 | } 20 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "fmt" 4 | 5 | // OperatorResourceKeyspace is the keyspace used for constructing application 6 | // metadata on Kubernetes objects 7 | const OperatorResourceKeyspace = "core.spinkube.dev" 8 | 9 | // ConstructResourceLabelKey is used when building operator-managed labels for 10 | // resources. 11 | func ConstructResourceLabelKey(kind string) string { 12 | return fmt.Sprintf("%s/%s", OperatorResourceKeyspace, kind) 13 | } 14 | 15 | // KnownExecutor is an enumeration of the executors that are well-known and 16 | // supported by the spin operator. 17 | type KnownExecutor string 18 | 19 | const ( 20 | ContainerDShimSpinExecutor = "containerd-shim-spin" 21 | CyclotronExecutor = "cyclotron" 22 | ) 23 | -------------------------------------------------------------------------------- /internal/controller/service.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 5 | "github.com/spinkube/spin-operator/pkg/spinapp" 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/util/intstr" 9 | ) 10 | 11 | // constructService builds a corev1.Service based on the configuration of a SpinApp. 12 | func constructService(app *spinv1alpha1.SpinApp) *corev1.Service { 13 | annotations := app.Spec.ServiceAnnotations 14 | if annotations == nil { 15 | annotations = map[string]string{} 16 | } 17 | 18 | labels := constructAppLabels(app) 19 | 20 | statusKey, statusValue := spinapp.ConstructStatusReadyLabel(app.Name) 21 | selector := map[string]string{statusKey: statusValue} 22 | 23 | svc := &corev1.Service{ 24 | TypeMeta: metav1.TypeMeta{ 25 | Kind: "Service", 26 | APIVersion: "v1", 27 | }, 28 | ObjectMeta: metav1.ObjectMeta{ 29 | Name: app.Name, 30 | Namespace: app.Namespace, 31 | Labels: labels, 32 | Annotations: annotations, 33 | }, 34 | Spec: corev1.ServiceSpec{ 35 | Type: corev1.ServiceTypeClusterIP, 36 | Ports: []corev1.ServicePort{ 37 | { 38 | Protocol: corev1.ProtocolTCP, 39 | TargetPort: intstr.IntOrString{ 40 | Type: intstr.String, 41 | StrVal: spinapp.HTTPPortName, 42 | }, 43 | Port: spinapp.DefaultHTTPPort, 44 | }, 45 | }, 46 | Selector: selector, 47 | }, 48 | } 49 | 50 | return svc 51 | } 52 | 53 | // constructAppLabels returns the labels to add to deployment/service 54 | // objects for the given SpinApp 55 | func constructAppLabels(app *spinv1alpha1.SpinApp) map[string]string { 56 | return map[string]string{ 57 | spinapp.NameLabelKey: app.Name, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/controller/service_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestConstructService(t *testing.T) { 10 | t.Parallel() 11 | 12 | app := minimalSpinApp() 13 | svc := constructService(app) 14 | 15 | // Omitting these is likely to result in client breakage, thus require a test 16 | // change. 17 | require.Equal(t, "Service", svc.TypeMeta.Kind) 18 | require.Equal(t, "v1", svc.TypeMeta.APIVersion) 19 | 20 | // We expect that the service object has the app name and nothing else. 21 | require.Equal(t, map[string]string{"core.spinkube.dev/app-name": "my-app"}, svc.ObjectMeta.Labels) 22 | // We expect that the service selector has the app status and nothing else. 23 | require.Equal(t, map[string]string{"core.spinkube.dev/app.my-app.status": "ready"}, svc.Spec.Selector) 24 | 25 | // We expect that the HTTP Port is part of the service. There's currently no 26 | // non-http implementations of a Spin trigger in Kubernetes, thus nothing that 27 | // would change this. 28 | require.Len(t, svc.Spec.Ports, 1) 29 | require.Equal(t, int32(80), svc.Spec.Ports[0].Port) 30 | require.Equal(t, "http-app", svc.Spec.Ports[0].TargetPort.StrVal) 31 | } 32 | -------------------------------------------------------------------------------- /internal/controller/spinappexecutor_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "sync" 22 | "testing" 23 | "time" 24 | 25 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 26 | "github.com/spinkube/spin-operator/internal/generics" 27 | "github.com/stretchr/testify/require" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 31 | controllerconfig "sigs.k8s.io/controller-runtime/pkg/config" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | "sigs.k8s.io/controller-runtime/pkg/manager" 34 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 35 | ) 36 | 37 | func setupExecutorController(t *testing.T) (*envTestState, ctrl.Manager, *SpinAppExecutorReconciler) { 38 | t.Helper() 39 | 40 | envTest := SetupEnvTest(t) 41 | 42 | opts := zap.Options{ 43 | Development: true, 44 | } 45 | logger := zap.New(zap.UseFlagOptions(&opts)) 46 | 47 | // B/c of https://github.com/kubernetes-sigs/controller-runtime/issues/2937 48 | skipNameValidation := true 49 | 50 | mgr, err := ctrl.NewManager(envTest.cfg, manager.Options{ 51 | Metrics: metricsserver.Options{BindAddress: "0"}, 52 | Scheme: envTest.scheme, 53 | // Provide a real logger to controllers - this means that when tests fail we 54 | // get to see the controller logs that lead to the failure - if we decide this 55 | // is too noisy then we can gate this behind an env var like SPINKUBE_TEST_LOGS. 56 | Logger: logger, 57 | Controller: controllerconfig.Controller{SkipNameValidation: &skipNameValidation}, 58 | }) 59 | 60 | require.NoError(t, err) 61 | 62 | ctrlr := &SpinAppExecutorReconciler{ 63 | Client: envTest.k8sClient, 64 | Scheme: envTest.scheme, 65 | Recorder: mgr.GetEventRecorderFor("spinappexecutor-reconciler"), 66 | } 67 | 68 | require.NoError(t, ctrlr.SetupWithManager(mgr)) 69 | 70 | return envTest, mgr, ctrlr 71 | } 72 | func TestSpinAppExecutorReconcile_StartupShutdown(t *testing.T) { 73 | t.Parallel() 74 | 75 | _, mgr, _ := setupExecutorController(t) 76 | 77 | ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) 78 | defer cancelFunc() 79 | require.NoError(t, mgr.Start(ctx)) 80 | } 81 | 82 | func TestSpinAppExecutorReconcile_ContainerDShimSpinExecutorCreate(t *testing.T) { 83 | t.Parallel() 84 | 85 | envTest, mgr, _ := setupExecutorController(t) 86 | 87 | ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) 88 | defer cancelFunc() 89 | 90 | var wg sync.WaitGroup 91 | wg.Add(1) 92 | go func() { 93 | require.NoError(t, mgr.Start(ctx)) 94 | wg.Done() 95 | }() 96 | 97 | executor := testContainerdShimSpinExecutor() 98 | list := &spinv1alpha1.SpinAppExecutorList{} 99 | require.NoError(t, envTest.k8sClient.List(ctx, list)) 100 | require.True(t, len(list.Items) == 0) 101 | require.NoError(t, envTest.k8sClient.Create(ctx, executor)) 102 | require.NoError(t, envTest.k8sClient.List(ctx, list)) 103 | require.True(t, len(list.Items) == 1) 104 | } 105 | 106 | func TestSpinAppExecutorReconcile_ContainerDShimSpinExecutorDelete(t *testing.T) { 107 | t.Parallel() 108 | 109 | envTest, mgr, _ := setupExecutorController(t) 110 | executor := testContainerdShimSpinExecutor() 111 | 112 | envTest.k8sClient = fake.NewClientBuilder().WithScheme(envTest.scheme).WithObjects( 113 | executor, 114 | ).Build() 115 | 116 | ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) 117 | defer cancelFunc() 118 | 119 | var wg sync.WaitGroup 120 | wg.Add(1) 121 | go func() { 122 | require.NoError(t, mgr.Start(ctx)) 123 | wg.Done() 124 | }() 125 | 126 | list := &spinv1alpha1.SpinAppExecutorList{} 127 | require.NoError(t, envTest.k8sClient.List(ctx, list)) 128 | require.True(t, len(list.Items) == 1) 129 | require.NoError(t, envTest.k8sClient.Delete(ctx, executor)) 130 | require.NoError(t, envTest.k8sClient.List(ctx, list)) 131 | require.True(t, len(list.Items) == 0) 132 | } 133 | 134 | func testContainerdShimSpinExecutor() *spinv1alpha1.SpinAppExecutor { 135 | return &spinv1alpha1.SpinAppExecutor{ 136 | ObjectMeta: metav1.ObjectMeta{ 137 | Name: "test-executor", 138 | Namespace: "default", 139 | }, 140 | Spec: spinv1alpha1.SpinAppExecutorSpec{ 141 | CreateDeployment: true, 142 | DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ 143 | RuntimeClassName: generics.Ptr("test-runtime"), 144 | }, 145 | }, 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/generics/generics.go: -------------------------------------------------------------------------------- 1 | package generics 2 | 3 | func MapList[A ~[]X, X, Y any](input A, mapper func(X) Y) []Y { 4 | result := make([]Y, len(input)) 5 | for idx, value := range input { 6 | result[idx] = mapper(value) 7 | } 8 | return result 9 | } 10 | 11 | func MapMap[X ~map[A]B, Y ~map[A]C, A comparable, B, C any](input X, mapper func(A, B) C) Y { 12 | result := make(Y, len(input)) 13 | for key, value := range input { 14 | result[key] = mapper(key, value) 15 | } 16 | return result 17 | } 18 | 19 | func AssociateBy[A ~[]X, X any, Y comparable](input A, assocBy func(X) Y) map[Y]X { 20 | result := make(map[Y]X, len(input)) 21 | for _, elem := range input { 22 | result[assocBy(elem)] = elem 23 | } 24 | return result 25 | } 26 | 27 | func Ptr[T any](v T) *T { 28 | return &v 29 | } 30 | -------------------------------------------------------------------------------- /internal/logging/logr_logger.go: -------------------------------------------------------------------------------- 1 | // Package logging provides the operators's recommended logging interface. 2 | // 3 | // The logging interface avoids the complexity of levels and provides a simpler 4 | // api that makes it harder to introduce unnecessary ambiguity to logs (or 5 | // ascribing value to arbitrary magic numbers). 6 | // 7 | // An Error logging helper exists primarily to facilitate including a stack trace 8 | // when the backing provider supports it. 9 | package logging 10 | 11 | import ( 12 | "context" 13 | 14 | "github.com/go-logr/logr" 15 | "sigs.k8s.io/controller-runtime/pkg/log" 16 | ) 17 | 18 | // A Logger logs messages. Messages may be supplemented by structured data. 19 | type Logger interface { 20 | // Info logs a message with optional structured data. Structured data must 21 | // be supplied as an array that alternates between string keys and values of 22 | // an arbitrary type. Use Info for messages that users are 23 | // very likely to be concerned with. 24 | Info(msg string, keysAndValues ...any) 25 | 26 | // Error logs a message with optional structured data. Structured data must 27 | // be supplied as an array that alternates between string keys and values of 28 | // an arbitrary type. Use Error when you want to enrich a message with as much 29 | // information as a logging provider can. 30 | Error(err error, msg string, keysAndValues ...any) 31 | 32 | // Debug logs a message with optional structured data. Structured data must 33 | // be supplied as an array that alternates between string keys and values of 34 | // an arbitrary type. Use Debug for messages that operators or 35 | // developers may be concerned with when debugging the operator or spin. 36 | Debug(msg string, keysAndValues ...any) 37 | 38 | // WithValues returns a Logger that will include the supplied structured 39 | // data with any subsequent messages it logs. Structured data must 40 | // be supplied as an array that alternates between string keys and values of 41 | // an arbitrary type. 42 | WithValues(keysAndValues ...any) Logger 43 | } 44 | 45 | // NewNopLogger returns a Logger that does nothing. 46 | func NewNopLogger() Logger { return nopLogger{} } 47 | 48 | type nopLogger struct{} 49 | 50 | func (l nopLogger) Info(_ string, _ ...any) {} 51 | func (l nopLogger) Debug(_ string, _ ...any) {} 52 | func (l nopLogger) Error(_ error, _ string, _ ...any) {} 53 | func (l nopLogger) WithValues(_ ...any) Logger { return nopLogger{} } 54 | 55 | // NewLogrLogger returns a Logger that is satisfied by the supplied logr.Logger, 56 | // which may be satisfied in turn by various logging implementations. 57 | // Debug messages are logged at V(1) - following the recommendation of 58 | // controller-runtime. 59 | func NewLogrLogger(l logr.Logger) Logger { 60 | return logrLogger{log: l} 61 | } 62 | 63 | type logrLogger struct { 64 | log logr.Logger 65 | } 66 | 67 | func (l logrLogger) Info(msg string, keysAndValues ...any) { 68 | l.log.Info(msg, keysAndValues...) 69 | } 70 | 71 | func (l logrLogger) Error(err error, msg string, keysAndValues ...any) { 72 | l.log.Error(err, msg, keysAndValues...) 73 | } 74 | 75 | func (l logrLogger) Debug(msg string, keysAndValues ...any) { 76 | l.log.V(1).Info(msg, keysAndValues...) 77 | } 78 | 79 | func (l logrLogger) WithValues(keysAndValues ...any) Logger { 80 | return logrLogger{log: l.log.WithValues(keysAndValues...)} 81 | } 82 | 83 | func FromContext(ctx context.Context) Logger { 84 | return logrLogger{log: log.FromContext(ctx)} 85 | } 86 | -------------------------------------------------------------------------------- /internal/runtimeconfig/types.go: -------------------------------------------------------------------------------- 1 | package runtimeconfig 2 | 3 | import ( 4 | "fmt" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "github.com/spinkube/spin-operator/pkg/secret" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | ) 11 | 12 | type EnvVariablesProviderOptions struct { 13 | Prefix string `toml:"prefix,omitempty"` 14 | DotEnvPath string `toml:"dotenv_path,omitempty"` 15 | } 16 | 17 | type VariablesProvider struct { 18 | Type string `toml:"type,omitempty"` 19 | EnvVariablesProviderOptions 20 | } 21 | 22 | type KeyValueStoreOptions map[string]secret.String 23 | 24 | type SQLiteDatabaseOptions map[string]secret.String 25 | 26 | type Spin struct { 27 | Variables []VariablesProvider `toml:"config_provider,omitempty"` 28 | 29 | KeyValueStores map[string]KeyValueStoreOptions `toml:"key_value_store,omitempty"` 30 | 31 | SQLiteDatabases map[string]SQLiteDatabaseOptions `toml:"sqlite_database,omitempty"` 32 | 33 | LLMCompute map[string]secret.String `toml:"llm_compute,omitempty"` 34 | } 35 | 36 | func (s *Spin) AddKeyValueStore( 37 | name, storeType, namespace string, 38 | secrets map[types.NamespacedName]*corev1.Secret, 39 | configMaps map[types.NamespacedName]*corev1.ConfigMap, 40 | opts []spinv1alpha1.RuntimeConfigOption) error { 41 | if s.KeyValueStores == nil { 42 | s.KeyValueStores = make(map[string]KeyValueStoreOptions) 43 | } 44 | 45 | if _, ok := s.KeyValueStores[name]; ok { 46 | return fmt.Errorf("duplicate definition for key value store with name: %s", name) 47 | } 48 | 49 | options, err := renderOptionsIntoMap(storeType, namespace, opts, secrets, configMaps) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | s.KeyValueStores[name] = options 55 | return nil 56 | } 57 | 58 | func (s *Spin) AddSQLiteDatabase( 59 | name, storeType, namespace string, 60 | secrets map[types.NamespacedName]*corev1.Secret, 61 | configMaps map[types.NamespacedName]*corev1.ConfigMap, 62 | opts []spinv1alpha1.RuntimeConfigOption) error { 63 | if s.SQLiteDatabases == nil { 64 | s.SQLiteDatabases = make(map[string]SQLiteDatabaseOptions) 65 | } 66 | if _, ok := s.SQLiteDatabases[name]; ok { 67 | return fmt.Errorf("duplicate definition for sqlite database with name: %s", name) 68 | } 69 | 70 | options, err := renderOptionsIntoMap(storeType, namespace, opts, secrets, configMaps) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | s.SQLiteDatabases[name] = SQLiteDatabaseOptions(options) 76 | return nil 77 | } 78 | 79 | func (s *Spin) AddLLMCompute(computeType, namespace string, 80 | secrets map[types.NamespacedName]*corev1.Secret, 81 | configMaps map[types.NamespacedName]*corev1.ConfigMap, 82 | opts []spinv1alpha1.RuntimeConfigOption) error { 83 | computeOpts, err := renderOptionsIntoMap(computeType, namespace, opts, secrets, configMaps) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | s.LLMCompute = computeOpts 89 | return nil 90 | } 91 | 92 | func renderOptionsIntoMap(typeOpt, namespace string, 93 | opts []spinv1alpha1.RuntimeConfigOption, 94 | secrets map[types.NamespacedName]*corev1.Secret, configMaps map[types.NamespacedName]*corev1.ConfigMap) (map[string]secret.String, error) { 95 | options := map[string]secret.String{ 96 | "type": secret.String(typeOpt), 97 | } 98 | 99 | for _, opt := range opts { 100 | var value string 101 | if opt.Value != "" { 102 | value = opt.Value 103 | } else if valueFrom := opt.ValueFrom; valueFrom != nil { 104 | if cmKeyRef := valueFrom.ConfigMapKeyRef; cmKeyRef != nil { 105 | cm, ok := configMaps[types.NamespacedName{Name: cmKeyRef.Name, Namespace: namespace}] 106 | if !ok { 107 | // This error shouldn't happen - we validate dependencies ahead of time, add this as a fallback error 108 | return nil, fmt.Errorf("unmet dependency while building config: configmap (%s/%s) not found", namespace, cmKeyRef.Name) 109 | } 110 | 111 | value = cm.Data[cmKeyRef.Key] 112 | } else if secKeyRef := valueFrom.SecretKeyRef; secKeyRef != nil { 113 | sec, ok := secrets[types.NamespacedName{Name: secKeyRef.Name, Namespace: namespace}] 114 | if !ok { 115 | // This error shouldn't happen - we validate dependencies ahead of time, add this as a fallback error 116 | return nil, fmt.Errorf("unmet dependency while building config: secret (%s/%s) not found", namespace, secKeyRef.Name) 117 | } 118 | 119 | value = string(sec.Data[secKeyRef.Key]) 120 | } 121 | } 122 | 123 | options[opt.Name] = secret.String(value) 124 | } 125 | 126 | return options, nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/webhook/admission.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 5 | ctrl "sigs.k8s.io/controller-runtime" 6 | ) 7 | 8 | func SetupSpinAppWebhookWithManager(mgr ctrl.Manager) error { 9 | return ctrl.NewWebhookManagedBy(mgr). 10 | For(&spinv1alpha1.SpinApp{}). 11 | WithDefaulter(&SpinAppDefaulter{Client: mgr.GetClient()}). 12 | WithValidator(&SpinAppValidator{Client: mgr.GetClient()}). 13 | Complete() 14 | } 15 | 16 | func SetupSpinAppExecutorWebhookWithManager(mgr ctrl.Manager) error { 17 | return ctrl.NewWebhookManagedBy(mgr). 18 | For(&spinv1alpha1.SpinAppExecutor{}). 19 | WithDefaulter(&SpinAppExecutorDefaulter{Client: mgr.GetClient()}). 20 | WithValidator(&SpinAppExecutorValidator{Client: mgr.GetClient()}). 21 | Complete() 22 | } 23 | -------------------------------------------------------------------------------- /internal/webhook/spinapp_defaulting.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 8 | "github.com/spinkube/spin-operator/internal/logging" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | // nolint:lll 14 | //+kubebuilder:webhook:path=/mutate-core-spinkube-dev-v1alpha1-spinapp,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.spinkube.dev,resources=spinapps,verbs=create;update,versions=v1alpha1,name=mspinapp.kb.io,admissionReviewVersions=v1 15 | 16 | // SpinAppDefaulter mutates SpinApps 17 | type SpinAppDefaulter struct { 18 | Client client.Client 19 | } 20 | 21 | // Default implements webhook.Defaulter 22 | func (d *SpinAppDefaulter) Default(ctx context.Context, obj runtime.Object) error { 23 | log := logging.FromContext(ctx) 24 | 25 | spinApp := obj.(*spinv1alpha1.SpinApp) 26 | log.Info("default", "name", spinApp.Name) 27 | 28 | if spinApp.Spec.Executor == "" { 29 | executor, err := d.findDefaultExecutor(ctx) 30 | if err != nil { 31 | return err 32 | } 33 | spinApp.Spec.Executor = executor 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // findDefaultExecutor sets the default executor for a SpinApp. 40 | // 41 | // Defaults to whatever executor is available on the cluster. If multiple 42 | // executors are available then the first executor in alphabetical order 43 | // will be chosen. If no executors are available then no default will be set. 44 | func (d *SpinAppDefaulter) findDefaultExecutor(ctx context.Context) (string, error) { 45 | log := logging.FromContext(ctx) 46 | 47 | var executors spinv1alpha1.SpinAppExecutorList 48 | if err := d.Client.List(ctx, &executors); err != nil { 49 | log.Error(err, "failed to list SpinAppExecutors") 50 | return "", err 51 | } 52 | 53 | if len(executors.Items) == 0 { 54 | log.Info("no SpinAppExecutors found") 55 | return "", nil 56 | } 57 | 58 | // Return first executor in alphabetical order 59 | chosenExecutor := executors.Items[0] 60 | for _, executor := range executors.Items[1:] { 61 | // For each item after the first see if it is alphabetically before the current chosen executor 62 | if strings.Compare(executor.Name, chosenExecutor.Name) < 0 { 63 | chosenExecutor = executor 64 | } 65 | } 66 | 67 | log.Info("defaulting to executor", "name", chosenExecutor.Name) 68 | return chosenExecutor.Name, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/webhook/spinapp_defaulting_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 8 | "github.com/spinkube/spin-operator/internal/constants" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestDefaultNothingToSet(t *testing.T) { 13 | t.Parallel() 14 | 15 | defaulter := &SpinAppDefaulter{} 16 | 17 | spinApp := &spinv1alpha1.SpinApp{Spec: spinv1alpha1.SpinAppSpec{ 18 | Executor: constants.CyclotronExecutor, 19 | Replicas: 1, 20 | }} 21 | 22 | err := defaulter.Default(context.Background(), spinApp) 23 | require.NoError(t, err) 24 | require.Equal(t, constants.CyclotronExecutor, spinApp.Spec.Executor) 25 | require.Equal(t, int32(1), spinApp.Spec.Replicas) 26 | } 27 | -------------------------------------------------------------------------------- /internal/webhook/spinapp_validating.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "github.com/spinkube/spin-operator/internal/logging" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/util/validation/field" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 15 | ) 16 | 17 | // nolint:lll 18 | //+kubebuilder:webhook:path=/validate-core-spinkube-dev-v1alpha1-spinapp,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.spinkube.dev,resources=spinapps,verbs=create;update,versions=v1alpha1,name=vspinapp.kb.io,admissionReviewVersions=v1 19 | 20 | // SpinAppValidator validates SpinApps 21 | type SpinAppValidator struct { 22 | Client client.Client 23 | } 24 | 25 | // ValidateCreate implements webhook.Validator 26 | func (v *SpinAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 27 | log := logging.FromContext(ctx) 28 | 29 | spinApp := obj.(*spinv1alpha1.SpinApp) 30 | log.Info("validate create", "name", spinApp.Name) 31 | 32 | return nil, v.validateSpinApp(ctx, spinApp) 33 | } 34 | 35 | // ValidateUpdate implements webhook.Validator 36 | func (v *SpinAppValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 37 | log := logging.FromContext(ctx) 38 | 39 | spinApp := newObj.(*spinv1alpha1.SpinApp) 40 | log.Info("validate update", "name", spinApp.Name) 41 | 42 | return nil, v.validateSpinApp(ctx, spinApp) 43 | } 44 | 45 | // ValidateDelete implements webhook.Validator 46 | func (v *SpinAppValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 47 | log := logging.FromContext(ctx) 48 | 49 | spinApp := obj.(*spinv1alpha1.SpinApp) 50 | log.Info("validate delete", "name", spinApp.Name) 51 | 52 | return nil, nil 53 | } 54 | 55 | func (v *SpinAppValidator) validateSpinApp(ctx context.Context, spinApp *spinv1alpha1.SpinApp) error { 56 | var allErrs field.ErrorList 57 | executor, err := validateExecutor(spinApp.Spec, v.fetchExecutor(ctx, spinApp.Namespace)) 58 | if err != nil { 59 | allErrs = append(allErrs, err) 60 | } 61 | if err := validateReplicas(spinApp.Spec); err != nil { 62 | allErrs = append(allErrs, err) 63 | } 64 | if err := validateAnnotations(spinApp.Spec, executor); err != nil { 65 | allErrs = append(allErrs, err) 66 | } 67 | 68 | if len(allErrs) == 0 { 69 | return nil 70 | } 71 | 72 | return apierrors.NewInvalid( 73 | schema.GroupKind{Group: "core.spinkube.dev", Kind: "SpinApp"}, 74 | spinApp.Name, allErrs) 75 | } 76 | 77 | // fetchExecutor returns a function that fetches a named executor in the provided namespace. 78 | // 79 | // We assume that the executor must exist in the same namespace as the SpinApp. 80 | func (v *SpinAppValidator) fetchExecutor(ctx context.Context, spinAppNs string) func(name string) (*spinv1alpha1.SpinAppExecutor, error) { 81 | return func(name string) (*spinv1alpha1.SpinAppExecutor, error) { 82 | var executor spinv1alpha1.SpinAppExecutor 83 | if err := v.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: spinAppNs}, &executor); err != nil { 84 | return nil, err 85 | } 86 | 87 | return &executor, nil 88 | } 89 | } 90 | 91 | func validateExecutor(spec spinv1alpha1.SpinAppSpec, fetchExecutor func(name string) (*spinv1alpha1.SpinAppExecutor, error)) (*spinv1alpha1.SpinAppExecutor, *field.Error) { 92 | if spec.Executor == "" { 93 | return nil, field.Invalid( 94 | field.NewPath("spec").Child("executor"), 95 | spec.Executor, 96 | "executor must be set, likely no default executor was set because you have no executors installed") 97 | } 98 | executor, err := fetchExecutor(spec.Executor) 99 | if err != nil { 100 | // Handle errors that are not just "Not Found" 101 | return nil, field.Invalid(field.NewPath("spec").Child("executor"), spec.Executor, "executor does not exist in namespace") 102 | } 103 | 104 | return executor, nil 105 | } 106 | 107 | func validateReplicas(spec spinv1alpha1.SpinAppSpec) *field.Error { 108 | if spec.EnableAutoscaling && spec.Replicas != 0 { 109 | return field.Invalid(field.NewPath("spec").Child("replicas"), spec.Replicas, "replicas cannot be set when autoscaling is enabled") 110 | } 111 | if !spec.EnableAutoscaling && spec.Replicas < 1 { 112 | return field.Invalid(field.NewPath("spec").Child("replicas"), spec.Replicas, "replicas must be > 0") 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func validateAnnotations(spec spinv1alpha1.SpinAppSpec, executor *spinv1alpha1.SpinAppExecutor) *field.Error { 119 | // We can't do any validation if the executor isn't available, but validation 120 | // will fail because of earlier errors. 121 | if executor == nil { 122 | return nil 123 | } 124 | 125 | if executor.Spec.CreateDeployment { 126 | return nil 127 | } 128 | // TODO: Make these validations opt in for executors? - Some runtimes may want these regardless. 129 | if len(spec.DeploymentAnnotations) != 0 { 130 | return field.Invalid( 131 | field.NewPath("spec").Child("deploymentAnnotations"), 132 | spec.DeploymentAnnotations, 133 | "deploymentAnnotations can't be set when the executor does not use operator deployments") 134 | } 135 | if len(spec.PodAnnotations) != 0 { 136 | return field.Invalid(field.NewPath("spec").Child("podAnnotations"), spec.PodAnnotations, "podAnnotations can't be set when the executor does not use operator deployments") 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/webhook/spinapp_validating_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 8 | "github.com/spinkube/spin-operator/internal/constants" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestValidateExecutor(t *testing.T) { 13 | t.Parallel() 14 | 15 | _, fldErr := validateExecutor(spinv1alpha1.SpinAppSpec{}, func(string) (*spinv1alpha1.SpinAppExecutor, error) { return nil, nil }) 16 | require.EqualError(t, fldErr, "spec.executor: Invalid value: \"\": executor must be set, likely no default executor was set because you have no executors installed") 17 | 18 | _, fldErr = validateExecutor( 19 | spinv1alpha1.SpinAppSpec{Executor: constants.CyclotronExecutor}, 20 | func(string) (*spinv1alpha1.SpinAppExecutor, error) { return nil, errors.New("executor not found?") }) 21 | require.EqualError(t, fldErr, "spec.executor: Invalid value: \"cyclotron\": executor does not exist in namespace") 22 | 23 | _, fldErr = validateExecutor(spinv1alpha1.SpinAppSpec{Executor: constants.ContainerDShimSpinExecutor}, func(string) (*spinv1alpha1.SpinAppExecutor, error) { return nil, nil }) 24 | require.Nil(t, fldErr) 25 | } 26 | 27 | func TestValidateReplicas(t *testing.T) { 28 | t.Parallel() 29 | 30 | fldErr := validateReplicas(spinv1alpha1.SpinAppSpec{}) 31 | require.EqualError(t, fldErr, "spec.replicas: Invalid value: 0: replicas must be > 0") 32 | 33 | fldErr = validateReplicas(spinv1alpha1.SpinAppSpec{Replicas: 1}) 34 | require.Nil(t, fldErr) 35 | } 36 | 37 | func TestValidateAnnotations(t *testing.T) { 38 | t.Parallel() 39 | 40 | deploymentlessExecutor := &spinv1alpha1.SpinAppExecutor{ 41 | Spec: spinv1alpha1.SpinAppExecutorSpec{ 42 | CreateDeployment: false, 43 | }, 44 | } 45 | deploymentfullExecutor := &spinv1alpha1.SpinAppExecutor{ 46 | Spec: spinv1alpha1.SpinAppExecutorSpec{ 47 | CreateDeployment: true, 48 | }, 49 | } 50 | 51 | fldErr := validateAnnotations(spinv1alpha1.SpinAppSpec{ 52 | Executor: "an-executor", 53 | DeploymentAnnotations: map[string]string{"key": "asdf"}, 54 | }, deploymentlessExecutor) 55 | require.EqualError(t, fldErr, 56 | `spec.deploymentAnnotations: Invalid value: map[string]string{"key":"asdf"}: `+ 57 | `deploymentAnnotations can't be set when the executor does not use operator deployments`) 58 | 59 | fldErr = validateAnnotations(spinv1alpha1.SpinAppSpec{ 60 | Executor: "an-executor", 61 | PodAnnotations: map[string]string{"key": "asdf"}, 62 | }, deploymentlessExecutor) 63 | require.EqualError(t, fldErr, 64 | `spec.podAnnotations: Invalid value: map[string]string{"key":"asdf"}: `+ 65 | `podAnnotations can't be set when the executor does not use operator deployments`) 66 | 67 | fldErr = validateAnnotations(spinv1alpha1.SpinAppSpec{ 68 | Executor: "an-executor", 69 | DeploymentAnnotations: map[string]string{"key": "asdf"}, 70 | }, deploymentfullExecutor) 71 | require.Nil(t, fldErr) 72 | 73 | fldErr = validateAnnotations(spinv1alpha1.SpinAppSpec{ 74 | Executor: "an-executor", 75 | }, deploymentlessExecutor) 76 | require.Nil(t, fldErr) 77 | } 78 | -------------------------------------------------------------------------------- /internal/webhook/spinappexecutor_defaulting.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "github.com/spinkube/spin-operator/internal/logging" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | // nolint:lll 13 | //+kubebuilder:webhook:path=/mutate-core-spinkube-dev-v1alpha1-spinappexecutor,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.spinkube.dev,resources=spinappexecutors,verbs=create;update,versions=v1alpha1,name=mspinappexecutor.kb.io,admissionReviewVersions=v1 14 | 15 | // SpinAppExecutorDefaulter mutates SpinApps 16 | type SpinAppExecutorDefaulter struct { 17 | Client client.Client 18 | } 19 | 20 | // Default implements webhook.Defaulter 21 | func (d *SpinAppExecutorDefaulter) Default(ctx context.Context, obj runtime.Object) error { 22 | log := logging.FromContext(ctx) 23 | 24 | executor := obj.(*spinv1alpha1.SpinAppExecutor) 25 | log.Info("default", "name", executor.Name) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/webhook/spinappexecutor_defaulting_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | // Currently the defaulting webhook is a no-op so nothing to test 4 | -------------------------------------------------------------------------------- /internal/webhook/spinappexecutor_validating.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "github.com/spinkube/spin-operator/internal/logging" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/util/validation/field" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 15 | ) 16 | 17 | // nolint:lll 18 | //+kubebuilder:webhook:path=/validate-core-spinkube-dev-v1alpha1-spinappexecutor,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.spinkube.dev,resources=spinappexecutors,verbs=create;update,versions=v1alpha1,name=vspinappexecutor.kb.io,admissionReviewVersions=v1 19 | 20 | // SpinAppExecutorValidator validates SpinApps 21 | type SpinAppExecutorValidator struct { 22 | Client client.Client 23 | } 24 | 25 | // ValidateCreate implements webhook.Validator 26 | func (v *SpinAppExecutorValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 27 | log := logging.FromContext(ctx) 28 | 29 | executor := obj.(*spinv1alpha1.SpinAppExecutor) 30 | log.Info("validate create", "name", executor.Name) 31 | 32 | return nil, v.validateSpinAppExecutor(executor) 33 | } 34 | 35 | // ValidateUpdate implements webhook.Validator 36 | func (v *SpinAppExecutorValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 37 | log := logging.FromContext(ctx) 38 | 39 | executor := newObj.(*spinv1alpha1.SpinAppExecutor) 40 | log.Info("validate update", "name", executor.Name) 41 | 42 | return nil, v.validateSpinAppExecutor(executor) 43 | } 44 | 45 | // ValidateDelete implements webhook.Validator 46 | func (v *SpinAppExecutorValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 47 | log := logging.FromContext(ctx) 48 | 49 | executor := obj.(*spinv1alpha1.SpinAppExecutor) 50 | log.Info("validate delete", "name", executor.Name) 51 | 52 | return nil, nil 53 | } 54 | 55 | func (v *SpinAppExecutorValidator) validateSpinAppExecutor(executor *spinv1alpha1.SpinAppExecutor) error { 56 | var allErrs field.ErrorList 57 | 58 | if err := validateRuntimeClassAndSpinImage(&executor.Spec); err != nil { 59 | allErrs = append(allErrs, err) 60 | } 61 | if len(allErrs) == 0 { 62 | return nil 63 | } 64 | 65 | return apierrors.NewInvalid( 66 | schema.GroupKind{Group: "core.spinkube.dev", Kind: "SpinAppExecutor"}, 67 | executor.Name, allErrs) 68 | } 69 | 70 | func validateRuntimeClassAndSpinImage(spec *spinv1alpha1.SpinAppExecutorSpec) *field.Error { 71 | if spec.DeploymentConfig == nil { 72 | return nil 73 | } 74 | 75 | if spec.DeploymentConfig.RuntimeClassName != nil && spec.DeploymentConfig.SpinImage != nil { 76 | return field.Invalid(field.NewPath("spec").Child("deploymentConfig").Child("runtimeClassName"), spec.DeploymentConfig.RuntimeClassName, 77 | "runtimeClassName and spinImage are mutually exclusive") 78 | } 79 | 80 | if spec.DeploymentConfig.RuntimeClassName == nil && spec.DeploymentConfig.SpinImage == nil { 81 | return field.Invalid(field.NewPath("spec").Child("deploymentConfig").Child("runtimeClassName"), spec.DeploymentConfig.RuntimeClassName, 82 | "either runtimeClassName or spinImage must be set") 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/webhook/spinappexecutor_validating_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "github.com/spinkube/spin-operator/internal/generics" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestValidateRuntimeClassAndSpinImage(t *testing.T) { 12 | t.Parallel() 13 | 14 | fldErr := validateRuntimeClassAndSpinImage(&spinv1alpha1.SpinAppExecutorSpec{ 15 | CreateDeployment: true, 16 | DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ 17 | RuntimeClassName: generics.Ptr("foo"), 18 | SpinImage: generics.Ptr("bar"), 19 | }, 20 | }) 21 | require.EqualError(t, fldErr, "spec.deploymentConfig.runtimeClassName: Invalid value: \"foo\": runtimeClassName and spinImage are mutually exclusive") 22 | 23 | fldErr = validateRuntimeClassAndSpinImage(&spinv1alpha1.SpinAppExecutorSpec{ 24 | CreateDeployment: true, 25 | DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ 26 | RuntimeClassName: generics.Ptr("foo"), 27 | SpinImage: nil, 28 | }, 29 | }) 30 | require.Nil(t, fldErr) 31 | 32 | fldErr = validateRuntimeClassAndSpinImage(&spinv1alpha1.SpinAppExecutorSpec{ 33 | CreateDeployment: true, 34 | DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ 35 | RuntimeClassName: nil, 36 | SpinImage: generics.Ptr("bar"), 37 | }, 38 | }) 39 | require.Nil(t, fldErr) 40 | 41 | fldErr = validateRuntimeClassAndSpinImage(&spinv1alpha1.SpinAppExecutorSpec{ 42 | CreateDeployment: true, 43 | DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ 44 | RuntimeClassName: nil, 45 | SpinImage: nil, 46 | }, 47 | }) 48 | require.EqualError(t, fldErr, "spec.deploymentConfig.runtimeClassName: Invalid value: \"null\": either runtimeClassName or spinImage must be set") 49 | } 50 | -------------------------------------------------------------------------------- /pkg/secret/secret.go: -------------------------------------------------------------------------------- 1 | // Secret is a package for types that make it harder to accidentally expose 2 | // secret variables when passing them around. 3 | package secret 4 | 5 | type String string 6 | 7 | const redacted = "REDACTED" 8 | 9 | // String implements fmt.Stringer and redacts the sensitive value. 10 | func (s String) String() string { 11 | return redacted 12 | } 13 | 14 | // GoString implements fmt.GoStringer and redacts the sensitive value. 15 | func (s String) GoString() string { 16 | return redacted 17 | } 18 | 19 | // Value returns the sensitive value as a string. 20 | func (s String) Value() string { 21 | return string(s) 22 | } 23 | 24 | func (s String) MarshalJSON() ([]byte, error) { 25 | return []byte(`"` + redacted + `"`), nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/secret/secret_test.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | toml "github.com/pelletier/go-toml/v2" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSecret(t *testing.T) { 13 | s := String("secret") 14 | require.Equal(t, "secret", s.Value(), "secret") 15 | require.Equal(t, "REDACTED", fmt.Sprintf("%v", s)) 16 | require.Equal(t, "REDACTED", s.String()) 17 | require.Equal(t, "REDACTED", s.GoString()) 18 | 19 | b, err := json.Marshal(s) 20 | require.NoError(t, err) 21 | require.Equal(t, `"REDACTED"`, string(b)) 22 | } 23 | 24 | func TestSecret_TOMLMarshal(t *testing.T) { 25 | toMarshal := map[string]String{ 26 | "some_key": String("some_secret_value"), 27 | } 28 | data, err := toml.Marshal(toMarshal) 29 | require.NoError(t, err) 30 | // If `toml` changes its default marshal formatting it is fine to update this 31 | // test to match - we only care that the secret is rendered in plain text. 32 | require.Equal(t, "some_key = 'some_secret_value'\n", string(data)) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/spinapp/spinapp.go: -------------------------------------------------------------------------------- 1 | package spinapp 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spinkube/spin-operator/internal/constants" 7 | ) 8 | 9 | const ( 10 | // HTTPPortName is the name used to identify the HTTP Port on a spin app 11 | // deployment. 12 | HTTPPortName = "http-app" 13 | 14 | // DefaultHTTPPort is the port that the operator will assign to a pod by 15 | // default when constructing deployments and services. 16 | DefaultHTTPPort = 80 17 | 18 | // StatusReady is the ready value for an app status label. 19 | StatusReady = "ready" 20 | ) 21 | 22 | var ( 23 | // NameLabelKey is the app name label key. 24 | NameLabelKey = constants.ConstructResourceLabelKey("app-name") 25 | ) 26 | 27 | // ConstructStatusLabelKey returns the app status label key, used primarily 28 | // in Service selectors. 29 | func ConstructStatusLabelKey(appName string) string { 30 | return constants.ConstructResourceLabelKey(fmt.Sprintf("app.%s.status", appName)) 31 | } 32 | 33 | // ConstructStatusReadyLabel returns the app status label key and value used 34 | // by a Service selector to select Pod(s) ready to serve an app. 35 | func ConstructStatusReadyLabel(appName string) (string, string) { 36 | return ConstructStatusLabelKey(appName), StatusReady 37 | } 38 | -------------------------------------------------------------------------------- /scripts/update-chart-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Note: using '-i.bak' to support different versions of sed when using in-place editing. 5 | 6 | # Swap tag in for main for URLs if the version is vx.x.x* 7 | if [[ "${APP_VERSION}" =~ ^v[0-9]+.[0-9]+.[0-9]+(.*)? ]]; then 8 | sed -i.bak -e "s%spinkube/spin-operator/main%spinkube/spin-operator/${APP_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/README.md" 9 | sed -i.bak -e "s%spinkube/spin-operator/main%spinkube/spin-operator/${APP_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/templates/NOTES.txt" 10 | fi 11 | 12 | ## Update Chart.yaml with CHART_VERSION and APP_VERSION 13 | sed -r -i.bak -e "s%^version: .*%version: ${CHART_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/Chart.yaml" 14 | sed -r -i.bak -e "s%^appVersion: .*%appVersion: ${APP_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/Chart.yaml" 15 | 16 | ## Update README.md with CHART_VERSION 17 | sed -i.bak -e "s%{{ CHART_VERSION }}%${CHART_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/README.md" 18 | 19 | # Cleanup 20 | find "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}" -type f -name '*.bak' -print0 | xargs -0 rm -- --------------------------------------------------------------------------------