├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── contrib └── spin-plugin.json.tmpl ├── go.mod ├── go.sum ├── main.go ├── pkg ├── cmd │ ├── connect.go │ ├── delete.go │ ├── deploy.go │ ├── factory.go │ ├── get.go │ ├── list.go │ ├── logs.go │ ├── output.go │ ├── prompt.go │ ├── root.go │ ├── scaffold.go │ ├── scaffold_test.go │ ├── testdata │ │ ├── azure_workload_identity.yml │ │ ├── components.yml │ │ ├── hpa_autoscaler.yml │ │ ├── hpa_service_account.yml │ │ ├── keda_autoscaler.yml │ │ ├── multiple_image_secrets.yml │ │ ├── no_service_account_name.yml │ │ ├── one_image_secret.yml │ │ ├── runtime-config.toml │ │ ├── scaffold_image.yml │ │ ├── scaffold_runtime_config.yml │ │ ├── service_account_name.yml │ │ └── variables.yml │ └── version.go └── kube │ └── spin.go ├── release-process.md └── spin-pluginify.toml /.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 | * @bacongobbler @rajatjindal 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: gomod 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | open-pull-requests-limit: 10 14 | groups: 15 | all-dependencies: 16 | patterns: 17 | - "*" 18 | update-types: 19 | - "minor" 20 | - "patch" 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: stable 24 | cache: false 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v6 27 | with: 28 | version: v1.64.6 29 | args: --timeout 3m 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | goreleaser: 10 | permissions: 11 | # grant the github token the ability to modify release tags 12 | contents: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | # fetch all history for goreleaser to work correctly 19 | # https://goreleaser.com/ci/actions/#workflow 20 | fetch-depth: 0 21 | 22 | - name: Setup Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: stable 26 | 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v5 29 | with: 30 | version: latest 31 | args: release --clean ${{ github.ref == 'refs/heads/main' && '--snapshot' || '' }} 32 | env: 33 | GITHUB_TOKEN: ${{ github.token }} 34 | 35 | - name: Release Plugin 36 | uses: rajatjindal/spin-plugin-releaser@v1 37 | with: 38 | github_token: ${{ github.token }} 39 | upload_checksums: true 40 | template_file: contrib/spin-plugin.json.tmpl 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | GOPRIVATE: "github.com/spinkube/spin-operator" 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: stable 24 | 25 | - name: Build 26 | run: go build -o spin-plugin-kube main.go 27 | 28 | - name: Run tests 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | /bin 3 | 4 | # output from `spin pluginify --install` 5 | *.tar.gz 6 | kube.json 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: spin-plugin-kube 3 | main: ./ 4 | binary: kube 5 | env: 6 | - CGO_ENABLED=0 7 | ldflags: 8 | - "-s -w -X github.com/spinkube/spin-plugin-kube/pkg/cmd.Version={{.Version}}" 9 | goos: 10 | - darwin 11 | - linux 12 | - windows 13 | goarch: 14 | - amd64 15 | - arm64 16 | 17 | archives: 18 | - builds: 19 | - spin-plugin-kube 20 | name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 21 | wrap_in_directory: false 22 | format: tar.gz 23 | files: 24 | - LICENSE 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project subscribes to the Fermyon [Code of Conduct](https://www.fermyon.com/code-of-conduct). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) The Spin Framework Contributors, Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COMMIT = $(shell git rev-parse --short HEAD) 2 | 3 | .PHONY: build 4 | build: 5 | go build -ldflags "-X github.com/spinkube/spin-plugin-kube/pkg/cmd.Version=git-${COMMIT}" -o bin/spin-kube . 6 | 7 | .PHONY: install 8 | install: 9 | spin pluginify --install 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spin kube plugin 2 | 3 | A [Spin plugin](https://github.com/fermyon/spin-plugins) for interacting with Kubernetes. 4 | 5 | ## Install 6 | 7 | Install the stable release: 8 | 9 | ```sh 10 | spin plugins update 11 | spin plugins install kube 12 | ``` 13 | 14 | ### Compiling from source 15 | 16 | As an alternative to the plugin manager, you can download and manually install the plugin. Manual installation is 17 | commonly used to test in-flight changes. For a user, it's better to install the plugin using Spin's plugin manager. 18 | 19 | Ensure the `pluginify` plugin is installed: 20 | 21 | ```sh 22 | spin plugins update 23 | spin plugins install pluginify --yes 24 | ``` 25 | 26 | Fetch the plugin: 27 | 28 | ```sh 29 | git clone git@github.com:spinkube/spin-plugin-kube.git 30 | cd spin-plugin-kube 31 | ``` 32 | 33 | Compile and install the plugin: 34 | 35 | ```sh 36 | make 37 | make install 38 | ``` 39 | 40 | ## Prerequisites 41 | 42 | Ensure SpinKube is installed in your Kubernetes cluster. See the [SpinKube Quickstart 43 | Guide](https://www.spinkube.dev/docs/install/quickstart/). 44 | 45 | ## Usage 46 | 47 | Install the wasm32-wasi target for Rust: 48 | 49 | ```sh 50 | rustup target add wasm32-wasi 51 | ``` 52 | 53 | Create a Spin application: 54 | 55 | ```sh 56 | spin new --accept-defaults -t http-rust hello-rust 57 | cd hello-rust 58 | ``` 59 | 60 | Compile the application: 61 | 62 | ```sh 63 | spin build 64 | ``` 65 | 66 | Publish your application: 67 | 68 | ```sh 69 | docker login 70 | spin registry push bacongobbler/hello-rust:latest 71 | ``` 72 | 73 | Deploy to Kubernetes: 74 | 75 | ```sh 76 | spin kube scaffold --from bacongobbler/hello-rust:latest | kubectl create -f - 77 | ``` 78 | 79 | View your application: 80 | 81 | ```sh 82 | kubectl get spinapps 83 | ``` 84 | 85 | `spin kube scaffold` deploys two replicas by default. You can change this with the `--replicas` flag: 86 | 87 | ```sh 88 | spin kube scaffold --from bacongobbler/hello-rust:latest --replicas 3 | kubectl apply -f - 89 | ``` 90 | 91 | Delete the app: 92 | 93 | ```sh 94 | kubectl delete spinapp hello-rust 95 | ``` 96 | 97 | ### Autoscaler support 98 | 99 | Autoscaler support can be enabled by setting `--autoscaler` and by setting a CPU limit and a memory limit. 100 | 101 | ```sh 102 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi 103 | ``` 104 | 105 | Setting min/max replicas: 106 | 107 | ```sh 108 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --replicas 1 --max-replicas 10 109 | ``` 110 | 111 | CPU/memory limits and CPU/memory requests can be set together: 112 | 113 | ```sh 114 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --cpu-request 50m --memory-request 64Mi 115 | ``` 116 | 117 | ```text 118 | IMPORTANT! 119 | CPU/memory requests are optional and will default to the CPU/memory limit if not set. 120 | CPU/memory requests must be lower than their respective CPU/memory limit. 121 | ``` 122 | 123 | Setting the target CPU utilization: 124 | 125 | ```sh 126 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --autoscaler-target-cpu-utilization 50 127 | ``` 128 | 129 | Setting the target memory utilization: 130 | 131 | ```sh 132 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler hpa --cpu-limit 100m --memory-limit 128Mi --autoscaler-target-memory-utilization 50 133 | ``` 134 | 135 | KEDA support: 136 | 137 | ```sh 138 | spin kube scaffold --from bacongobbler/hello-rust:latest --autoscaler keda --cpu-limit 100m --memory-limit 128Mi 139 | ``` 140 | 141 | ### Working with images from private registries 142 | 143 | Support for pulling images from private registries can be enabled by using `--image-pull-secret ` flag, where `` is a secret of type [`docker-registry`](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets) in same namespace as your SpinApp. 144 | 145 | To enable multiple private registries, you can provide the flag `--image-pull-secret` multiple times with secret for each registry that you wish to use. 146 | 147 | Create a secret with credentials for private registry 148 | 149 | ```sh 150 | $) kubectl create secret docker-registry registry-credentials \ 151 | --docker-server=ghcr.io \ 152 | --docker-username=bacongobbler \ 153 | --docker-password=github-token 154 | 155 | secret/registry-credentials created 156 | ``` 157 | 158 | Verify that the secret is created 159 | 160 | ```sh 161 | $) kubectl get secret registry-credentials -o yaml 162 | 163 | apiVersion: v1 164 | data: 165 | .dockerconfigjson: eyJhdXRocyI6eyJnaGNyLmlvIjp7InVzZXJuYW1lIjoiYmFjb25nb2JibGVyIiwicGFzc3dvcmQiOiJnaXRodWItdG9rZW4iLCJhdXRoIjoiWW1GamIyNW5iMkppYkdWeU9tZHBkR2gxWWkxMGIydGxiZz09In19fQ== 166 | kind: Secret 167 | metadata: 168 | creationTimestamp: "2024-02-27T02:18:53Z" 169 | name: registry-credentials 170 | namespace: default 171 | resourceVersion: "162287" 172 | uid: 2e12ddd1-919d-44b5-b6cc-c3cd5c09fcec 173 | type: kubernetes.io/dockerconfigjson 174 | ``` 175 | 176 | Use the secret when scaffolding the SpinApp 177 | 178 | ```sh 179 | $) spin kube scaffold --from bacongobbler/hello-rust:latest --image-pull-secret registry-credentials 180 | 181 | apiVersion: core.spinkube.dev/v1alpha1 182 | kind: SpinApp 183 | metadata: 184 | name: hello-rust 185 | spec: 186 | image: "bacongobbler/hello-rust:latest" 187 | executor: containerd-shim-spin 188 | replicas: 2 189 | imagePullSecrets: 190 | - name: registry-credentials 191 | ``` 192 | -------------------------------------------------------------------------------- /contrib/spin-plugin.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kube", 3 | "description": "A plugin to manage spin apps", 4 | "homepage": "https://github.com/spinframework/spin-plugin-kube", 5 | "version": "{{ Version }}", 6 | "spinCompatibility": ">=2.3.1", 7 | "license": "Apache-2.0", 8 | "packages": [ 9 | { 10 | "os": "linux", 11 | "arch": "amd64", 12 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-linux-amd64.tar.gz{{/addURLAndSha}} 13 | }, 14 | { 15 | "os": "linux", 16 | "arch": "aarch64", 17 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-linux-arm64.tar.gz{{/addURLAndSha}} 18 | }, 19 | { 20 | "os": "macos", 21 | "arch": "aarch64", 22 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-darwin-arm64.tar.gz{{/addURLAndSha}} 23 | }, 24 | { 25 | "os": "macos", 26 | "arch": "amd64", 27 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-darwin-amd64.tar.gz{{/addURLAndSha}} 28 | }, 29 | { 30 | "os": "windows", 31 | "arch": "amd64", 32 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-windows-amd64.tar.gz{{/addURLAndSha}} 33 | }, 34 | { 35 | "os": "windows", 36 | "arch": "aarch64", 37 | {{#addURLAndSha}}https://github.com/spinframework/spin-plugin-kube/releases/download/{{ TagName }}/spin-plugin-kube-{{ Version }}-windows-arm64.tar.gz{{/addURLAndSha}} 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spinkube/spin-plugin-kube 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/gosuri/uitable v0.0.4 9 | github.com/novln/docker-parser v1.0.0 10 | github.com/pelletier/go-toml/v2 v2.2.3 11 | github.com/spf13/cobra v1.8.1 12 | github.com/spf13/pflag v1.0.5 13 | github.com/spinkube/spin-operator v0.5.0 14 | github.com/stretchr/testify v1.10.0 15 | k8s.io/api v0.32.3 16 | k8s.io/apimachinery v0.32.3 17 | k8s.io/cli-runtime v0.29.1 18 | k8s.io/client-go v0.32.3 19 | k8s.io/kubectl v0.29.1 20 | sigs.k8s.io/controller-runtime v0.20.3 21 | ) 22 | 23 | require ( 24 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 25 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 26 | github.com/chai2010/gettext-go v1.0.2 // indirect 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 28 | github.com/emicklei/go-restful/v3 v3.11.2 // indirect 29 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 30 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 31 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 32 | github.com/fatih/camelcase v1.0.0 // indirect 33 | github.com/fatih/color v1.16.0 // indirect 34 | github.com/fvbommel/sortorder v1.1.0 // indirect 35 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 36 | github.com/go-errors/errors v1.5.1 // indirect 37 | github.com/go-logr/logr v1.4.2 // indirect 38 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 39 | github.com/go-openapi/jsonreference v0.20.4 // indirect 40 | github.com/go-openapi/swag v0.23.0 // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/protobuf v1.5.4 // indirect 43 | github.com/google/btree v1.1.3 // 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/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/gorilla/websocket v1.5.1 // indirect 50 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 51 | github.com/imdario/mergo v0.3.16 // indirect 52 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 56 | github.com/mailru/easyjson v0.7.7 // indirect 57 | github.com/mattn/go-colorable v0.1.13 // indirect 58 | github.com/mattn/go-isatty v0.0.20 // indirect 59 | github.com/mattn/go-runewidth v0.0.15 // indirect 60 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 61 | github.com/moby/spdystream v0.5.0 // indirect 62 | github.com/moby/term v0.5.0 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 66 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 67 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 68 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 69 | github.com/pkg/errors v0.9.1 // indirect 70 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 71 | github.com/rivo/uniseg v0.4.4 // indirect 72 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 73 | github.com/x448/float16 v0.8.4 // indirect 74 | github.com/xlab/treeprint v1.2.0 // indirect 75 | go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect 76 | golang.org/x/net v0.35.0 // indirect 77 | golang.org/x/oauth2 v0.25.0 // indirect 78 | golang.org/x/sync v0.12.0 // indirect 79 | golang.org/x/sys v0.30.0 // indirect 80 | golang.org/x/term v0.29.0 // indirect 81 | golang.org/x/text v0.22.0 // indirect 82 | golang.org/x/time v0.7.0 // indirect 83 | google.golang.org/protobuf v1.36.5 // indirect 84 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 85 | gopkg.in/evanphx/json-patch.v5 v5.8.1 // indirect 86 | gopkg.in/inf.v0 v0.9.1 // indirect 87 | gopkg.in/yaml.v2 v2.4.0 // indirect 88 | gopkg.in/yaml.v3 v3.0.1 // indirect 89 | k8s.io/component-base v0.32.3 // indirect 90 | k8s.io/klog/v2 v2.130.1 // indirect 91 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 92 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 93 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 94 | sigs.k8s.io/kustomize/api v0.16.0 // indirect 95 | sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect 96 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 97 | sigs.k8s.io/yaml v1.4.0 // indirect 98 | ) 99 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 4 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 5 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 7 | github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= 8 | github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 10 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 11 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= 17 | github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 18 | github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= 19 | github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 20 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 21 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 22 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= 23 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= 24 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 25 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 26 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 27 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 28 | github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= 29 | github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 30 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 31 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 32 | github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= 33 | github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 34 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 35 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 36 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 37 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 38 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 39 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 40 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 41 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 42 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 43 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 44 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 45 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 46 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 47 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 48 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 49 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 50 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 51 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 52 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 53 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 54 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 55 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 56 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 57 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 58 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 59 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 60 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 61 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 62 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 63 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 64 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 65 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 66 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 67 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 68 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 69 | github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= 70 | github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 71 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= 72 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 73 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 74 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 75 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 76 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 77 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 78 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 79 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 80 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 81 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 82 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 83 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 84 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 85 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 86 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 87 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 88 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 89 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 90 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 91 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 92 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 93 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 94 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 95 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 96 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 97 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 98 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 99 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 100 | github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= 101 | github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 102 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 103 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 104 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 105 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 107 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 108 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 109 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 110 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 111 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 112 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 113 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 114 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 115 | github.com/novln/docker-parser v1.0.0 h1:PjEBd9QnKixcWczNGyEdfUrP6GR0YUilAqG7Wksg3uc= 116 | github.com/novln/docker-parser v1.0.0/go.mod h1:oCeM32fsoUwkwByB5wVjsrsVQySzPWkl3JdlTn1txpE= 117 | github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= 118 | github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 119 | github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= 120 | github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 121 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 122 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 123 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 124 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 125 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 126 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 127 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 128 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 129 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 130 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 131 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 132 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 133 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 134 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 135 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 136 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 137 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 138 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 139 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 140 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 141 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 142 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 143 | github.com/spinkube/spin-operator v0.5.0 h1:b0BlJijv59w6lINNo9DxmUIOsiKxjbb58lCy9Urh+Sk= 144 | github.com/spinkube/spin-operator v0.5.0/go.mod h1:wPUhDcYWKQxbFg38bVOqYJhlMbeJR6dCk9NtYS9w65k= 145 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 146 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 147 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 148 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 149 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 150 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 151 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 152 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 153 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 154 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 155 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 156 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 157 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 158 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 159 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 160 | go.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4= 161 | go.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= 162 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 163 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 164 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 165 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 166 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 167 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 168 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 169 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 170 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 171 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 172 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 173 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 174 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 176 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 177 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 178 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 179 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 180 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 181 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 184 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 185 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 186 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 193 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 194 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 195 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 196 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 197 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 198 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 199 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 200 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 201 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 202 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 203 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 204 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 205 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 206 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 207 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 208 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 210 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 211 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 213 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 214 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 215 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 216 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 217 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 218 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 219 | gopkg.in/evanphx/json-patch.v5 v5.8.1 h1:BVxXj2YS+4i9fttNkVvDKi4Pg1pVMpVE8tdEwaKeQY0= 220 | gopkg.in/evanphx/json-patch.v5 v5.8.1/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= 221 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 222 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 223 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 224 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 225 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 226 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 227 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 228 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 229 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 230 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 231 | k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= 232 | k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= 233 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 234 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 235 | k8s.io/cli-runtime v0.29.1 h1:By3WVOlEWYfyxhGko0f/IuAOLQcbBSMzwSaDren2JUs= 236 | k8s.io/cli-runtime v0.29.1/go.mod h1:vjEY9slFp8j8UoMhV5AlO8uulX9xk6ogfIesHobyBDU= 237 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 238 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 239 | k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= 240 | k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= 241 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 242 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 243 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 244 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 245 | k8s.io/kubectl v0.29.1 h1:rWnW3hi/rEUvvg7jp4iYB68qW5un/urKbv7fu3Vj0/s= 246 | k8s.io/kubectl v0.29.1/go.mod h1:SZzvLqtuOJYSvZzPZR9weSuP0wDQ+N37CENJf0FhDF4= 247 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 248 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 249 | sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= 250 | sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= 251 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 252 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 253 | sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= 254 | sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c= 255 | sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= 256 | sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= 257 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 258 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 259 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 260 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 261 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spinkube/spin-plugin-kube/pkg/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /pkg/cmd/connect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | appsv1 "k8s.io/api/apps/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/kubectl/pkg/cmd/logs" 13 | "k8s.io/kubectl/pkg/cmd/portforward" 14 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 15 | "k8s.io/kubectl/pkg/polymorphichelpers" 16 | ) 17 | 18 | const spinAppPort = "80" 19 | 20 | var connectCmd = &cobra.Command{ 21 | Use: "connect ", 22 | Short: "Establish a connection to a running application", 23 | Hidden: isExperimentalFlagNotSet, 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | var appName string 26 | if len(args) > 0 { 27 | appName = args[0] 28 | } 29 | 30 | if appName == "" && appNameFromCurrentDirContext != "" { 31 | appName = appNameFromCurrentDirContext 32 | } 33 | 34 | localPort, err := cmd.Flags().GetString("local-port") 35 | if err != nil { 36 | return err 37 | } 38 | fieldSelector, err := cmd.Flags().GetString("field-selector") 39 | if err != nil { 40 | return err 41 | } 42 | labelSelector, err := cmd.Flags().GetString("label-selector") 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if appName == "" && fieldSelector == "" && labelSelector == "" { 48 | return fmt.Errorf("either one of , --field-selector, or --label-selector is required") 49 | } 50 | 51 | getPodTimeout, err := cmdutil.GetPodRunningTimeoutFlag(cmd) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | kubeclient, err := getKubernetesClientset() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | resp, err := kubeclient.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{ 62 | LabelSelector: labelSelector, 63 | FieldSelector: fieldSelector, 64 | }) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(resp.Items) == 0 { 70 | return fmt.Errorf("no active deployment found for the given application name or selector") 71 | } 72 | 73 | var deploy appsv1.Deployment 74 | if appName != "" { 75 | for _, item := range resp.Items { 76 | if item.Name == appName { 77 | deploy = item 78 | break 79 | } 80 | } 81 | } else { 82 | deploy = resp.Items[0] 83 | } 84 | 85 | factory, streams := NewCommandFactory() 86 | pod, err := polymorphichelpers.AttachablePodForObjectFn(factory, &deploy, getPodTimeout) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | fmt.Printf("connecting to pod %s/%s\n", pod.Namespace, pod.Name) 92 | reference := fmt.Sprintf("pod/%s", pod.Name) 93 | if strings.Contains(localPort, ":") { 94 | return fmt.Errorf("local port should not contain ':' character") 95 | } 96 | 97 | go func() { 98 | logOpts = logs.NewLogsOptions(streams, false) 99 | logOpts.Follow = true 100 | 101 | lccmd := logs.NewCmdLogs(factory, streams) 102 | 103 | cmdutil.CheckErr(logOpts.Complete(factory, lccmd, []string{reference})) 104 | cmdutil.CheckErr(logOpts.Validate()) 105 | cmdutil.CheckErr(logOpts.RunLogs()) 106 | }() 107 | 108 | ccmd := portforward.NewCmdPortForward(factory, streams) 109 | ccmd.Run(ccmd, []string{reference, fmt.Sprintf("%s:%s", localPort, spinAppPort)}) 110 | 111 | return nil 112 | }, 113 | } 114 | 115 | func init() { 116 | cmdutil.AddPodRunningTimeoutFlag(connectCmd, 30*time.Second) 117 | configFlags.AddFlags(connectCmd.Flags()) 118 | 119 | connectCmd.Flags().StringP("local-port", "p", "", "The local port to listen on when connecting to SpinApp") 120 | connectCmd.Flags().String("field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") 121 | connectCmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") 122 | 123 | rootCmd.AddCommand(connectCmd) 124 | } 125 | -------------------------------------------------------------------------------- /pkg/cmd/delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | apierrors "k8s.io/apimachinery/pkg/api/errors" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | var deleteCmd = &cobra.Command{ 14 | Use: "delete ", 15 | Short: "Delete the application", 16 | Hidden: isExperimentalFlagNotSet, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | var appName string 19 | if len(args) > 0 { 20 | appName = args[0] 21 | } 22 | 23 | if appName == "" { 24 | return fmt.Errorf("no application name specified to delete") 25 | } 26 | 27 | yes, err := cmd.Flags().GetBool("yes") 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if !yes { 33 | yes, err = yesOrNo("This action is irreversible. Are you sure? (y/N): ") 34 | if err != nil { 35 | return err 36 | } 37 | } 38 | 39 | if !yes { 40 | return nil 41 | } 42 | 43 | okey := client.ObjectKey{ 44 | Namespace: namespace, 45 | Name: appName, 46 | } 47 | 48 | err = kubeImpl.DeleteSpinApp(context.TODO(), okey) 49 | if err != nil { 50 | if apierrors.IsNotFound(err) { 51 | fmt.Printf("Could not find application with name %s\n", appName) 52 | os.Exit(1) 53 | } 54 | 55 | fmt.Println(err) 56 | os.Exit(1) 57 | } 58 | 59 | fmt.Printf("Succesfully deleted %s\n", appName) 60 | return nil 61 | }, 62 | } 63 | 64 | func init() { 65 | configFlags.AddFlags(deleteCmd.Flags()) 66 | 67 | deleteCmd.Flags().BoolP("yes", "y", false, "specify --yes to immediately delete the application") 68 | rootCmd.AddCommand(deleteCmd) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/cmd/deploy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/spf13/cobra" 10 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/cli-runtime/pkg/printers" 13 | ) 14 | 15 | var ( 16 | artifact string 17 | replicas int32 18 | dryRun bool 19 | ) 20 | 21 | var deployCmd = &cobra.Command{ 22 | Use: "deploy", 23 | Short: "Deploy application to Kubernetes", 24 | Hidden: isExperimentalFlagNotSet, 25 | RunE: func(_ *cobra.Command, _ []string) error { 26 | name, err := getNameFromImageReference(artifact) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | spinapp := spinv1alpha1.SpinApp{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: name, 34 | Namespace: namespace, 35 | }, 36 | TypeMeta: metav1.TypeMeta{ 37 | APIVersion: "core.spinkube.dev/v1alpha1", 38 | Kind: "SpinApp", 39 | }, 40 | Spec: spinv1alpha1.SpinAppSpec{ 41 | Replicas: replicas, 42 | Image: artifact, 43 | Executor: "containerd-shim-spin", 44 | }, 45 | } 46 | 47 | if dryRun { 48 | y := printers.YAMLPrinter{} 49 | if err := y.PrintObj(&spinapp, os.Stdout); err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | if err := kubeImpl.ApplySpinApp(context.TODO(), &spinapp); err != nil { 56 | return err 57 | } 58 | 59 | fmt.Printf("spinapp.spin.fermyon.com/%s configured\n", name) 60 | return nil 61 | }, 62 | } 63 | 64 | func init() { 65 | deployCmd.Flags().BoolVar(&dryRun, "dry-run", false, "only print the kubernetes manifest without deploying") 66 | deployCmd.Flags().Int32VarP(&replicas, "replicas", "r", 2, "Number of replicas for the application") 67 | deployCmd.Flags().StringVarP(&artifact, "from", "f", "", "Reference in the registry of the application") 68 | 69 | if err := deployCmd.MarkFlagRequired("from"); err != nil { 70 | log.Fatal(err) 71 | } 72 | 73 | configFlags.AddFlags(deployCmd.Flags()) 74 | rootCmd.AddCommand(deployCmd) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/cmd/factory.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | "k8s.io/client-go/kubernetes" 11 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 12 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | func NewCommandFactory() (cmdutil.Factory, genericclioptions.IOStreams) { 17 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(configFlags) 18 | return cmdutil.NewFactory(matchVersionKubeConfigFlags), 19 | genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} 20 | } 21 | 22 | func getRuntimeClient() (client.Client, error) { 23 | var scheme = runtime.NewScheme() 24 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 25 | utilruntime.Must(spinv1alpha1.AddToScheme(scheme)) 26 | 27 | config, err := configFlags.ToRESTConfig() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return client.New(config, client.Options{ 33 | Scheme: scheme, 34 | }) 35 | } 36 | 37 | func getKubernetesClientset() (kubernetes.Interface, error) { 38 | config, err := configFlags.ToRESTConfig() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return kubernetes.NewForConfig(config) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/cmd/get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | var getCmd = &cobra.Command{ 12 | Use: "get ", 13 | Short: "Display detailed application information", 14 | Hidden: isExperimentalFlagNotSet, 15 | RunE: func(_ *cobra.Command, args []string) error { 16 | var appName string 17 | if len(args) > 0 { 18 | appName = args[0] 19 | } 20 | 21 | if appName == "" && appNameFromCurrentDirContext != "" { 22 | appName = appNameFromCurrentDirContext 23 | } 24 | 25 | okey := client.ObjectKey{ 26 | Namespace: namespace, 27 | Name: appName, 28 | } 29 | 30 | app, err := kubeImpl.GetSpinApp(context.TODO(), okey) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | printApps(os.Stdout, app) 36 | return nil 37 | }, 38 | } 39 | 40 | func init() { 41 | configFlags.AddFlags(getCmd.Flags()) 42 | rootCmd.AddCommand(getCmd) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var listCmd = &cobra.Command{ 11 | Use: "list", 12 | Short: "List applications", 13 | Hidden: isExperimentalFlagNotSet, 14 | RunE: func(_ *cobra.Command, _ []string) error { 15 | appsResp, err := kubeImpl.ListSpinApps(context.TODO(), namespace) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | printApps(os.Stdout, appsResp.Items...) 21 | 22 | return nil 23 | }, 24 | } 25 | 26 | func init() { 27 | configFlags.AddFlags(listCmd.Flags()) 28 | rootCmd.AddCommand(listCmd) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/cmd/logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "k8s.io/kubectl/pkg/cmd/logs" 8 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 9 | ) 10 | 11 | var logOpts *logs.LogsOptions 12 | 13 | var logsCmd = &cobra.Command{ 14 | Use: "logs ", 15 | Short: "Display application logs", 16 | Hidden: isExperimentalFlagNotSet, 17 | Run: func(_ *cobra.Command, args []string) { 18 | var appName string 19 | if len(args) > 0 { 20 | appName = args[0] 21 | } 22 | 23 | if appName == "" && appNameFromCurrentDirContext != "" { 24 | appName = appNameFromCurrentDirContext 25 | } 26 | 27 | reference := fmt.Sprintf("deployment/%s", appName) 28 | 29 | factory, streams := NewCommandFactory() 30 | ccmd := logs.NewCmdLogs(factory, streams) 31 | 32 | cmdutil.CheckErr(logOpts.Complete(factory, ccmd, []string{reference})) 33 | cmdutil.CheckErr(logOpts.Validate()) 34 | cmdutil.CheckErr(logOpts.RunLogs()) 35 | }, 36 | } 37 | 38 | func init() { 39 | _, streams := NewCommandFactory() 40 | logOpts = logs.NewLogsOptions(streams, false) 41 | logOpts.AddFlags(logsCmd) 42 | 43 | configFlags.AddFlags(logsCmd.Flags()) 44 | rootCmd.AddCommand(logsCmd) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/cmd/output.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/gosuri/uitable" 8 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 9 | ) 10 | 11 | func printApps(w io.Writer, apps ...spinv1alpha1.SpinApp) { 12 | table := uitable.New() 13 | table.MaxColWidth = 50 14 | table.AddRow("NAMESPACE", "NAME", "EXECUTOR", "READY") 15 | 16 | for _, app := range apps { 17 | table.AddRow(app.Namespace, app.Name, app.Spec.Executor, fmt.Sprintf("%d/%d", app.Status.ReadyReplicas, app.Spec.Replicas)) 18 | } 19 | 20 | fmt.Fprintln(w, table) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/cmd/prompt.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func yesOrNo(question string) (bool, error) { 11 | fmt.Print(question) 12 | reader := bufio.NewReader(os.Stdin) 13 | response, err := reader.ReadString('\n') 14 | if err != nil { 15 | return false, err 16 | } 17 | 18 | response = strings.ToLower(strings.TrimSpace(response)) 19 | if response == "y" || response == "yes" { 20 | return true, nil 21 | } 22 | 23 | return false, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/pelletier/go-toml/v2" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/pflag" 10 | "github.com/spinkube/spin-plugin-kube/pkg/kube" 11 | "k8s.io/cli-runtime/pkg/genericclioptions" 12 | _ "k8s.io/client-go/plugin/pkg/client/auth" // required for k8s client auth 13 | ) 14 | 15 | // global variables available to all sub-commands 16 | var ( 17 | appNameFromCurrentDirContext = "" 18 | configFlags = genericclioptions.NewConfigFlags(true) 19 | namespace string 20 | kubeImpl *kube.Impl 21 | isExperimentalFlagNotSet = os.Getenv("SPIN_EXPERIMENTAL") == "" 22 | ) 23 | 24 | // rootCmd represents the base command when called without any subcommands 25 | var rootCmd = newRootCmd() 26 | 27 | func newRootCmd() *cobra.Command { 28 | root := &cobra.Command{ 29 | Use: "kube", 30 | Short: "Manage applications running on Kubernetes", 31 | Version: Version, 32 | PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 33 | namespace = getNamespace(configFlags) 34 | k8sclient, err := getRuntimeClient() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | kubeImpl = kube.New(k8sclient, configFlags) 40 | 41 | appNameFromCurrentDirContext, err = initAppNameFromCurrentDirContext() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | }, 48 | } 49 | 50 | flagSet := pflag.NewFlagSet("kubectl", pflag.ExitOnError) 51 | configFlags.AddFlags(flagSet) 52 | flagSet.VisitAll(func(f *pflag.Flag) { 53 | // disable shorthand for all kubectl flags 54 | f.Shorthand = "" 55 | // mark all as hidden 56 | f.Hidden = true 57 | 58 | switch f.Name { 59 | case "kubeconfig": 60 | f.Hidden = false 61 | f.Usage = "the path to the kubeconfig file" 62 | case "namespace": 63 | f.Hidden = false 64 | // restore the shorthand for --namespace 65 | f.Shorthand = "n" 66 | f.Usage = "the namespace scope" 67 | default: 68 | // unless explicitly listed above, we prefix all kubectl flags with "kube-" so they don't clash with our own 69 | // flags 70 | f.Name = "kube-" + f.Name 71 | } 72 | }) 73 | root.Flags().AddFlagSet(flagSet) 74 | return root 75 | } 76 | 77 | // Execute adds all child commands to the root command and sets flags appropriately. 78 | // This is called by main.main(). It only needs to happen once to the rootCmd. 79 | func Execute() { 80 | if err := rootCmd.Execute(); err != nil { 81 | os.Exit(1) 82 | } 83 | } 84 | 85 | // getNamespace takes a set of kubectl flag values and returns the namespace we should be operating in 86 | func getNamespace(flags *genericclioptions.ConfigFlags) string { 87 | namespace, _, err := flags.ToRawKubeConfigLoader().Namespace() 88 | if err != nil || len(namespace) == 0 { 89 | namespace = "default" 90 | } 91 | 92 | return namespace 93 | } 94 | 95 | func initAppNameFromCurrentDirContext() (string, error) { 96 | if strings.ToLower(os.Getenv("SPIN_KUBE_DISABLE_DIR_CONTEXT")) == "true" { 97 | return "", nil 98 | } 99 | 100 | content, err := os.ReadFile("spin.toml") 101 | // running from a non spin-app dir 102 | if os.IsNotExist(err) { 103 | return "", nil 104 | } 105 | 106 | manifest := struct { 107 | Application struct { 108 | Name string `toml:"name"` 109 | } `toml:"application"` 110 | }{} 111 | 112 | err = toml.Unmarshal(content, &manifest) 113 | if err != nil { 114 | return "", err 115 | } 116 | 117 | return manifest.Application.Name, nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/cmd/scaffold.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "text/template" 11 | 12 | dockerparser "github.com/novln/docker-parser" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type ScaffoldOptions struct { 17 | autoscaler string 18 | azureWorkloadIdentity bool 19 | configfile string 20 | cpuLimit string 21 | cpuRequest string 22 | executor string 23 | from string 24 | imagePullSecrets []string 25 | maxReplicas int32 26 | memoryLimit string 27 | memoryRequest string 28 | output string 29 | replicas int32 30 | serviceAccountName string 31 | targetCPUUtilizationPercentage int32 32 | targetMemoryUtilizationPercentage int32 33 | variables map[string]string 34 | components []string 35 | } 36 | 37 | var scaffoldOpts = ScaffoldOptions{} 38 | 39 | type appConfig struct { 40 | Autoscaler string 41 | AzureWorkloadIdentity bool 42 | CPULimit string 43 | CPURequest string 44 | Executor string 45 | Image string 46 | ImagePullSecrets []string 47 | MaxReplicas int32 48 | MemoryLimit string 49 | MemoryRequest string 50 | Name string 51 | Replicas int32 52 | RuntimeConfig string 53 | ServiceAccountName string 54 | TargetCPUUtilizationPercentage int32 55 | TargetMemoryUtilizationPercentage int32 56 | Variables map[string]string 57 | Components []string 58 | } 59 | 60 | var manifestStr = `apiVersion: core.spinkube.dev/v1alpha1 61 | kind: SpinApp 62 | metadata: 63 | name: {{ .Name }} 64 | spec: 65 | image: "{{ .Image }}" 66 | executor: {{ .Executor }} 67 | {{- if not (eq .Autoscaler "") }} 68 | enableAutoscaling: true 69 | {{- else }} 70 | replicas: {{ .Replicas }} 71 | {{- end}} 72 | {{- if .AzureWorkloadIdentity }} 73 | podLabels: 74 | azure.workload.identity/use: "true" 75 | {{- end }} 76 | {{- if .ServiceAccountName }} 77 | serviceAccountName: {{ .ServiceAccountName }} 78 | {{- end }} 79 | {{- if .Variables }} 80 | variables: 81 | {{- range $key, $value := .Variables }} 82 | - name: {{ $key }} 83 | value: {{ $value }} 84 | {{- end }} 85 | {{- end }} 86 | {{- if .Components }} 87 | components: 88 | {{- range $c := .Components }} 89 | - {{ $c }} 90 | {{- end }} 91 | {{- end }} 92 | {{- if or .CPULimit .MemoryLimit }} 93 | resources: 94 | limits: 95 | {{- if .CPULimit }} 96 | cpu: {{ .CPULimit }} 97 | {{- end }} 98 | {{- if .MemoryLimit }} 99 | memory: {{ .MemoryLimit }} 100 | {{- end }} 101 | {{- if or .CPURequest .MemoryRequest }} 102 | requests: 103 | {{- if .CPURequest }} 104 | cpu: {{ .CPURequest }} 105 | {{- end }} 106 | {{- if .MemoryRequest }} 107 | memory: {{ .MemoryRequest }} 108 | {{- end }} 109 | {{- end }} 110 | {{- end }} 111 | {{- if len .ImagePullSecrets }} 112 | imagePullSecrets: 113 | {{- range $index, $secret := .ImagePullSecrets }} 114 | - name: {{ $secret -}} 115 | {{ end }} 116 | {{- end }} 117 | {{- if .RuntimeConfig }} 118 | runtimeConfig: 119 | loadFromSecret: {{ .Name }}-runtime-config 120 | --- 121 | apiVersion: v1 122 | kind: Secret 123 | metadata: 124 | name: {{ .Name }}-runtime-config 125 | type: Opaque 126 | data: 127 | runtime-config.toml: {{ .RuntimeConfig }} 128 | {{- end }} 129 | {{- if not (eq .Autoscaler "") }} 130 | --- 131 | {{- if eq .Autoscaler "hpa" }} 132 | apiVersion: autoscaling/v2 133 | kind: HorizontalPodAutoscaler 134 | metadata: 135 | name: {{ .Name }}-autoscaler 136 | spec: 137 | scaleTargetRef: 138 | apiVersion: apps/v1 139 | kind: Deployment 140 | name: {{ .Name }} 141 | minReplicas: {{ .Replicas }} 142 | maxReplicas: {{ .MaxReplicas }} 143 | metrics: 144 | - type: Resource 145 | resource: 146 | name: cpu 147 | target: 148 | type: Utilization 149 | averageUtilization: {{ .TargetCPUUtilizationPercentage }} 150 | - type: Resource 151 | resource: 152 | name: memory 153 | target: 154 | type: Utilization 155 | averageUtilization: {{ .TargetMemoryUtilizationPercentage }} 156 | {{- else if eq .Autoscaler "keda" }} 157 | apiVersion: keda.sh/v1alpha1 158 | kind: ScaledObject 159 | metadata: 160 | name: {{ .Name }}-autoscaler 161 | spec: 162 | scaleTargetRef: 163 | apiVersion: apps/v1 164 | kind: Deployment 165 | name: {{ .Name }} 166 | minReplicaCount: {{ .Replicas }} 167 | maxReplicaCount: {{ .MaxReplicas }} 168 | triggers: 169 | - type: cpu 170 | metricType: Utilization 171 | metadata: 172 | value: "{{ .TargetCPUUtilizationPercentage }}" 173 | - type: memory 174 | metricType: Utilization 175 | metadata: 176 | value: "{{ .TargetMemoryUtilizationPercentage }}" 177 | {{- end }} 178 | {{- end }} 179 | ` 180 | 181 | var scaffoldCmd = &cobra.Command{ 182 | Use: "scaffold", 183 | Short: "Scaffold application manifest", 184 | RunE: func(_ *cobra.Command, _ []string) error { 185 | content, err := scaffold(scaffoldOpts) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | if scaffoldOpts.output != "" { 191 | err = os.WriteFile(scaffoldOpts.output, content, 0600) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | log.Printf("\nApplication manifest saved to %s\n", scaffoldOpts.output) 197 | return nil 198 | 199 | } 200 | 201 | fmt.Fprint(os.Stdout, string(content)) 202 | 203 | return nil 204 | }, 205 | } 206 | 207 | func validateFlags(opts ScaffoldOptions) error { 208 | // replica count must be greater than 0 209 | if opts.replicas < 0 { 210 | return fmt.Errorf("the minimum replica count (%d) must be greater than 0", opts.replicas) 211 | } 212 | 213 | // check that the image reference is valid 214 | if !validateImageReference(opts.from) { 215 | return fmt.Errorf("invalid image reference provided: '%s'", opts.from) 216 | } 217 | 218 | // validate autoscaling flags 219 | // 220 | // NOTE: --replicas refers to the minimum number of replicas 221 | if opts.autoscaler != "" { 222 | // autoscaler type must be a valid type 223 | if opts.autoscaler != "hpa" && opts.autoscaler != "keda" { 224 | return fmt.Errorf("invalid autoscaler type '%s'; the autoscaler type must be either 'hpa' or 'keda'", opts.autoscaler) 225 | } 226 | 227 | // max replicas must be equal to or greater than 0 (scale down to 0 replicas is allowed) 228 | if opts.maxReplicas < 0 { 229 | return fmt.Errorf("the maximum replica count (%d) must be equal to or greater than 0", opts.maxReplicas) 230 | } 231 | 232 | // min replicas must be less than or equal to max replicas 233 | if opts.replicas > opts.maxReplicas { 234 | return fmt.Errorf("the minimum replica count (%d) must be less than or equal to the maximum replica count (%d)", opts.replicas, opts.maxReplicas) 235 | } 236 | 237 | // cpu and memory limits must be set 238 | if opts.cpuLimit == "" { 239 | return fmt.Errorf("cpu limits must be set when autoscaling is enabled") 240 | } 241 | 242 | if opts.memoryLimit == "" { 243 | return fmt.Errorf("memory limits must be set when autoscaling is enabled") 244 | } 245 | 246 | // TODO: cpu and memory requests must be lower than their respective cpu/memory limit 247 | 248 | // target cpu and memory utilization must be between 1 and 100 249 | if opts.targetCPUUtilizationPercentage < 1 || opts.targetCPUUtilizationPercentage > 100 { 250 | return fmt.Errorf("target cpu utilization percentage (%d) must be between 1 and 100", opts.targetCPUUtilizationPercentage) 251 | } 252 | 253 | if opts.targetMemoryUtilizationPercentage < 1 || opts.targetMemoryUtilizationPercentage > 100 { 254 | return fmt.Errorf("target memory utilization percentage (%d) must be between 1 and 100", opts.targetMemoryUtilizationPercentage) 255 | } 256 | } 257 | return nil 258 | } 259 | 260 | func scaffold(opts ScaffoldOptions) ([]byte, error) { 261 | if err := validateFlags(opts); err != nil { 262 | return nil, err 263 | } 264 | 265 | name, err := getNameFromImageReference(opts.from) 266 | if err != nil { 267 | return nil, err 268 | } 269 | 270 | config := appConfig{ 271 | Name: name, 272 | Image: opts.from, 273 | Replicas: opts.replicas, 274 | MaxReplicas: opts.maxReplicas, 275 | Executor: opts.executor, 276 | CPULimit: opts.cpuLimit, 277 | MemoryLimit: opts.memoryLimit, 278 | CPURequest: opts.cpuRequest, 279 | MemoryRequest: opts.memoryRequest, 280 | TargetCPUUtilizationPercentage: opts.targetCPUUtilizationPercentage, 281 | TargetMemoryUtilizationPercentage: opts.targetMemoryUtilizationPercentage, 282 | Autoscaler: opts.autoscaler, 283 | ImagePullSecrets: opts.imagePullSecrets, 284 | Variables: opts.variables, 285 | Components: opts.components, 286 | AzureWorkloadIdentity: opts.azureWorkloadIdentity, 287 | ServiceAccountName: opts.serviceAccountName, 288 | } 289 | 290 | if opts.configfile != "" { 291 | raw, readErr := os.ReadFile(opts.configfile) 292 | if readErr != nil { 293 | return nil, readErr 294 | } 295 | 296 | config.RuntimeConfig = base64.StdEncoding.EncodeToString(raw) 297 | } 298 | 299 | tmpl, err := template.New("spinapp").Parse(manifestStr) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | var output bytes.Buffer 305 | err = tmpl.Execute(&output, config) 306 | if err != nil { 307 | return nil, err 308 | } 309 | 310 | return output.Bytes(), nil 311 | } 312 | 313 | func validateImageReference(imageRef string) bool { 314 | _, err := dockerparser.Parse(imageRef) 315 | return err == nil 316 | } 317 | 318 | func getNameFromImageReference(imageRef string) (string, error) { 319 | ref, err := dockerparser.Parse(imageRef) 320 | if err != nil { 321 | return "", err 322 | } 323 | 324 | if strings.Contains(ref.ShortName(), "/") { 325 | parts := strings.Split(ref.ShortName(), "/") 326 | return parts[len(parts)-1], nil 327 | } 328 | 329 | return ref.ShortName(), nil 330 | } 331 | 332 | func init() { 333 | scaffoldCmd.Flags().Int32VarP(&scaffoldOpts.replicas, "replicas", "r", 2, "Minimum number of replicas for the application") 334 | scaffoldCmd.Flags().Int32Var(&scaffoldOpts.maxReplicas, "max-replicas", 3, "Maximum number of replicas for the application. Autoscaling must be enabled to use this flag") 335 | scaffoldCmd.Flags().Int32Var(&scaffoldOpts.targetCPUUtilizationPercentage, "autoscaler-target-cpu-utilization", 60, "The target CPU utilization percentage to maintain across all pods") 336 | scaffoldCmd.Flags().Int32Var(&scaffoldOpts.targetMemoryUtilizationPercentage, "autoscaler-target-memory-utilization", 60, "The target memory utilization percentage to maintain across all pods") 337 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.autoscaler, "autoscaler", "", "The autoscaler to use. Valid values are 'hpa' and 'keda'") 338 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.executor, "executor", "containerd-shim-spin", "The executor used to run the application") 339 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.cpuLimit, "cpu-limit", "", "The maximum amount of CPU resource units the application is allowed to use") 340 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.cpuRequest, "cpu-request", "", "The amount of CPU resource units requested by the application. Used to determine which node the application will run on") 341 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.memoryLimit, "memory-limit", "", "The maximum amount of memory the application is allowed to use") 342 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.memoryRequest, "memory-request", "", "The amount of memory requested by the application. Used to determine which node the application will run on") 343 | scaffoldCmd.Flags().BoolVar(&scaffoldOpts.azureWorkloadIdentity, "azure-identity", false, "Enable Azure Workload Identity for the application") 344 | scaffoldCmd.Flags().StringVar(&scaffoldOpts.serviceAccountName, "service-account-name", "", "The name of the service account to use for the application") 345 | scaffoldCmd.Flags().StringVarP(&scaffoldOpts.from, "from", "f", "", "Reference in the registry of the application") 346 | scaffoldCmd.Flags().StringVarP(&scaffoldOpts.output, "out", "o", "", "Path to file to write manifest yaml") 347 | scaffoldCmd.Flags().StringVarP(&scaffoldOpts.configfile, "runtime-config-file", "c", "", "Path to runtime config file") 348 | scaffoldCmd.Flags().StringSliceVarP(&scaffoldOpts.imagePullSecrets, "image-pull-secret", "s", []string{}, "Secrets in the same namespace to use for pulling the image") 349 | scaffoldCmd.PersistentFlags().StringToStringVarP(&scaffoldOpts.variables, "variable", "v", nil, "Application variable (name=value) to be provided to the application") 350 | scaffoldCmd.PersistentFlags().StringSliceVarP(&scaffoldOpts.components, "component", "", nil, "Component ID to run. This can be specified multiple times. The default is all components.") 351 | 352 | if err := scaffoldCmd.MarkFlagRequired("from"); err != nil { 353 | log.Fatal(err) 354 | } 355 | 356 | rootCmd.AddCommand(scaffoldCmd) 357 | } 358 | -------------------------------------------------------------------------------- /pkg/cmd/scaffold_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestScaffoldOutput(t *testing.T) { 12 | testcases := []struct { 13 | name string 14 | opts ScaffoldOptions 15 | expected string 16 | }{ 17 | { 18 | name: "only image is provided", 19 | opts: ScaffoldOptions{ 20 | from: "ghcr.io/foo/example-app:v0.1.0", 21 | replicas: 2, 22 | executor: "containerd-shim-spin", 23 | }, 24 | expected: "scaffold_image.yml", 25 | }, 26 | { 27 | name: "runtime config is provided", 28 | opts: ScaffoldOptions{ 29 | from: "ghcr.io/foo/example-app:v0.1.0", 30 | replicas: 2, 31 | executor: "containerd-shim-spin", 32 | configfile: "testdata/runtime-config.toml", 33 | }, 34 | expected: "scaffold_runtime_config.yml", 35 | }, 36 | { 37 | name: "azure identity is enabled", 38 | opts: ScaffoldOptions{ 39 | from: "ghcr.io/foo/example-app:v0.1.0", 40 | replicas: 2, 41 | executor: "containerd-shim-spin", 42 | azureWorkloadIdentity: true, 43 | }, 44 | expected: "azure_workload_identity.yml", 45 | }, 46 | { 47 | name: "one image pull secret is provided", 48 | opts: ScaffoldOptions{ 49 | from: "ghcr.io/foo/example-app:v0.1.0", 50 | replicas: 2, 51 | executor: "containerd-shim-spin", 52 | configfile: "testdata/runtime-config.toml", 53 | imagePullSecrets: []string{"secret-name"}, 54 | }, 55 | expected: "one_image_secret.yml", 56 | }, 57 | { 58 | name: "multiple image pull secrets are provided", 59 | opts: ScaffoldOptions{ 60 | from: "ghcr.io/foo/example-app:v0.1.0", 61 | replicas: 2, 62 | executor: "containerd-shim-spin", 63 | configfile: "testdata/runtime-config.toml", 64 | imagePullSecrets: []string{"secret-name", "secret-name-2"}, 65 | }, 66 | expected: "multiple_image_secrets.yml", 67 | }, 68 | { 69 | name: "service account name is provided", 70 | opts: ScaffoldOptions{ 71 | from: "ghcr.io/foo/example-app:v0.1.0", 72 | replicas: 2, 73 | executor: "containerd-shim-spin", 74 | serviceAccountName: "my-service-account", 75 | }, 76 | expected: "service_account_name.yml", 77 | }, 78 | { 79 | name: "service account name is not provided", 80 | opts: ScaffoldOptions{ 81 | from: "ghcr.io/foo/example-app:v0.1.0", 82 | replicas: 2, 83 | executor: "containerd-shim-spin", 84 | serviceAccountName: "", 85 | }, 86 | expected: "no_service_account_name.yml", 87 | }, 88 | { 89 | name: "service account with HPA autoscaler", 90 | opts: ScaffoldOptions{ 91 | from: "ghcr.io/foo/example-app:v0.1.0", 92 | executor: "containerd-shim-spin", 93 | autoscaler: "hpa", 94 | cpuLimit: "100m", 95 | memoryLimit: "128Mi", 96 | replicas: 2, 97 | maxReplicas: 3, 98 | targetCPUUtilizationPercentage: 60, 99 | targetMemoryUtilizationPercentage: 60, 100 | serviceAccountName: "my-service-account", 101 | }, 102 | expected: "hpa_service_account.yml", 103 | }, 104 | { 105 | name: "HPA autoscaler support", 106 | opts: ScaffoldOptions{ 107 | from: "ghcr.io/foo/example-app:v0.1.0", 108 | executor: "containerd-shim-spin", 109 | autoscaler: "hpa", 110 | cpuLimit: "100m", 111 | memoryLimit: "128Mi", 112 | replicas: 2, 113 | maxReplicas: 3, 114 | targetCPUUtilizationPercentage: 60, 115 | targetMemoryUtilizationPercentage: 60, 116 | }, 117 | expected: "hpa_autoscaler.yml", 118 | }, 119 | { 120 | name: "KEDA autoscaler support", 121 | opts: ScaffoldOptions{ 122 | from: "ghcr.io/foo/example-app:v0.1.0", 123 | executor: "containerd-shim-spin", 124 | autoscaler: "keda", 125 | cpuLimit: "100m", 126 | memoryLimit: "128Mi", 127 | replicas: 2, 128 | maxReplicas: 3, 129 | targetCPUUtilizationPercentage: 60, 130 | targetMemoryUtilizationPercentage: 60, 131 | }, 132 | expected: "keda_autoscaler.yml", 133 | }, 134 | { 135 | name: "variables are provided", 136 | opts: ScaffoldOptions{ 137 | from: "ghcr.io/foo/example-app:v0.1.0", 138 | replicas: 2, 139 | executor: "containerd-shim-spin", 140 | variables: map[string]string{ 141 | "bar": "yee", 142 | "foo": "yoo", 143 | }, 144 | }, 145 | expected: "variables.yml", 146 | }, 147 | { 148 | name: "components are specified", 149 | opts: ScaffoldOptions{ 150 | from: "ghcr.io/foo/example-app:v0.1.0", 151 | replicas: 2, 152 | executor: "containerd-shim-spin", 153 | components: []string{ 154 | "hello", 155 | "world", 156 | }, 157 | }, 158 | expected: "components.yml", 159 | }, 160 | } 161 | 162 | for _, tc := range testcases { 163 | t.Run(tc.name, func(t *testing.T) { 164 | output, err := scaffold(tc.opts) 165 | require.Nil(t, err) 166 | 167 | expectedContent, err := os.ReadFile(filepath.Join("testdata", tc.expected)) 168 | require.Nil(t, err) 169 | 170 | require.Equal(t, string(expectedContent), string(output)) 171 | }) 172 | } 173 | } 174 | 175 | func TestValidateImageReference_ValidImageReference(t *testing.T) { 176 | testCases := []string{ 177 | "bacongobbler/hello-rust", 178 | "bacongobbler/hello-rust:v1.0.0", 179 | "ghcr.io/bacongobbler/hello-rust", 180 | "ghcr.io/bacongobbler/hello-rust:v1.0.0", 181 | "ghcr.io/spinkube/spinkube/runtime-class-manager:v1", 182 | "nginx:latest", 183 | "nginx", 184 | "ttl.sh/hello-spinkube@sha256:cc4b191d11728b4e9e024308f0c03aded893da2002403943adc9deb8c4ca1644", 185 | } 186 | 187 | for _, tc := range testCases { 188 | t.Run(tc, func(t *testing.T) { 189 | valid := validateImageReference(tc) 190 | require.True(t, valid, "Expected image reference to be valid") 191 | }) 192 | 193 | } 194 | } 195 | 196 | func TestGetNameFromImageReference(t *testing.T) { 197 | testCases := []struct { 198 | reference string 199 | name string 200 | }{ 201 | { 202 | reference: "bacongobbler/hello-rust", 203 | name: "hello-rust", 204 | }, { 205 | reference: "bacongobbler/hello-rust:v1.0.0", 206 | name: "hello-rust", 207 | }, { 208 | 209 | reference: "ghcr.io/bacongobbler/hello-rust", 210 | name: "hello-rust", 211 | }, { 212 | reference: "ghcr.io/bacongobbler/hello-rust:v1.0.0", 213 | name: "hello-rust", 214 | }, { 215 | reference: "ghcr.io/spinkube/spinkube/runtime-class-manager:v1", 216 | name: "runtime-class-manager", 217 | }, { 218 | reference: "nginx:latest", 219 | name: "nginx", 220 | }, { 221 | reference: "nginx", 222 | name: "nginx", 223 | }, { 224 | reference: "ttl.sh/hello-spinkube@sha256:cc4b191d11728b4e9e024308f0c03aded893da2002403943adc9deb8c4ca1644", 225 | name: "hello-spinkube", 226 | }, 227 | } 228 | 229 | for _, tc := range testCases { 230 | t.Run(tc.reference, func(t *testing.T) { 231 | actualName, err := getNameFromImageReference(tc.reference) 232 | require.Nil(t, err) 233 | require.Equal(t, tc.name, actualName, "Expected image name from reference") 234 | }) 235 | } 236 | } 237 | 238 | func TestFlagValidation(t *testing.T) { 239 | testcases := []struct { 240 | name string 241 | opts ScaffoldOptions 242 | expectedError string 243 | }{ 244 | { 245 | name: "valid HPA autoscaling options", 246 | opts: ScaffoldOptions{ 247 | from: "ghcr.io/foo/example-app:v0.1.0", 248 | replicas: 2, 249 | maxReplicas: 5, 250 | executor: "containerd-shim-spin", 251 | autoscaler: "hpa", 252 | cpuLimit: "50m", 253 | memoryLimit: "100Mi", 254 | targetCPUUtilizationPercentage: 1, 255 | targetMemoryUtilizationPercentage: 1, 256 | }, 257 | }, 258 | { 259 | name: "valid KEDA autoscaling options", 260 | opts: ScaffoldOptions{ 261 | from: "ghcr.io/foo/example-app:v0.1.0", 262 | replicas: 2, 263 | maxReplicas: 5, 264 | executor: "containerd-shim-spin", 265 | autoscaler: "keda", 266 | cpuLimit: "50m", 267 | memoryLimit: "100Mi", 268 | targetCPUUtilizationPercentage: 1, 269 | targetMemoryUtilizationPercentage: 1, 270 | }, 271 | }, 272 | { 273 | name: "invalid replica count", 274 | opts: ScaffoldOptions{ 275 | from: "ghcr.io/foo/example-app:v0.1.0", 276 | replicas: -1, 277 | executor: "containerd-shim-spin", 278 | }, 279 | expectedError: "the minimum replica count (-1) must be greater than 0", 280 | }, 281 | { 282 | name: "invalid image reference", 283 | opts: ScaffoldOptions{ 284 | from: "invalid image reference!", 285 | executor: "containerd-shim-spin", 286 | }, 287 | expectedError: "invalid image reference provided: 'invalid image reference!'", 288 | }, 289 | { 290 | name: "invalid autoscaler type", 291 | opts: ScaffoldOptions{ 292 | from: "ghcr.io/foo/example-app:v0.1.0", 293 | autoscaler: "invalid", 294 | }, 295 | expectedError: "invalid autoscaler type 'invalid'; the autoscaler type must be either 'hpa' or 'keda'", 296 | }, 297 | { 298 | name: "max replica count less than zero", 299 | opts: ScaffoldOptions{ 300 | from: "ghcr.io/foo/example-app:v0.1.0", 301 | autoscaler: "hpa", 302 | maxReplicas: -1, 303 | }, 304 | expectedError: "the maximum replica count (-1) must be equal to or greater than 0", 305 | }, 306 | { 307 | name: "max replica count less than replica count", 308 | opts: ScaffoldOptions{ 309 | from: "ghcr.io/foo/example-app:v0.1.0", 310 | autoscaler: "hpa", 311 | replicas: 5, 312 | maxReplicas: 2, 313 | }, 314 | expectedError: "the minimum replica count (5) must be less than or equal to the maximum replica count (2)", 315 | }, 316 | { 317 | name: "must set cpu limits for HPA", 318 | opts: ScaffoldOptions{ 319 | from: "ghcr.io/foo/example-app:v0.1.0", 320 | autoscaler: "hpa", 321 | }, 322 | expectedError: "cpu limits must be set when autoscaling is enabled", 323 | }, 324 | { 325 | name: "must set memory limits for HPA", 326 | opts: ScaffoldOptions{ 327 | from: "ghcr.io/foo/example-app:v0.1.0", 328 | autoscaler: "hpa", 329 | cpuLimit: "50m", 330 | }, 331 | expectedError: "memory limits must be set when autoscaling is enabled", 332 | }, 333 | { 334 | name: "must set target cpu utilization percentage for HPA", 335 | opts: ScaffoldOptions{ 336 | from: "ghcr.io/foo/example-app:v0.1.0", 337 | autoscaler: "hpa", 338 | cpuLimit: "50m", 339 | memoryLimit: "100Mi", 340 | }, 341 | expectedError: "target cpu utilization percentage (0) must be between 1 and 100", 342 | }, 343 | { 344 | name: "must set target memory utilization percentage for HPA", 345 | opts: ScaffoldOptions{ 346 | from: "ghcr.io/foo/example-app:v0.1.0", 347 | autoscaler: "hpa", 348 | cpuLimit: "50m", 349 | memoryLimit: "100Mi", 350 | targetCPUUtilizationPercentage: 1, 351 | }, 352 | expectedError: "target memory utilization percentage (0) must be between 1 and 100", 353 | }, 354 | } 355 | 356 | for _, tc := range testcases { 357 | t.Run(tc.name, func(t *testing.T) { 358 | _, err := scaffold(tc.opts) 359 | 360 | if tc.expectedError == "" { 361 | require.Nil(t, err) 362 | } else { 363 | require.NotNil(t, err) 364 | require.Equal(t, tc.expectedError, err.Error()) 365 | } 366 | }) 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/azure_workload_identity.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | podLabels: 10 | azure.workload.identity/use: "true" 11 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/components.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | components: 10 | - hello 11 | - world 12 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/hpa_autoscaler.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | enableAutoscaling: true 9 | resources: 10 | limits: 11 | cpu: 100m 12 | memory: 128Mi 13 | --- 14 | apiVersion: autoscaling/v2 15 | kind: HorizontalPodAutoscaler 16 | metadata: 17 | name: example-app-autoscaler 18 | spec: 19 | scaleTargetRef: 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | name: example-app 23 | minReplicas: 2 24 | maxReplicas: 3 25 | metrics: 26 | - type: Resource 27 | resource: 28 | name: cpu 29 | target: 30 | type: Utilization 31 | averageUtilization: 60 32 | - type: Resource 33 | resource: 34 | name: memory 35 | target: 36 | type: Utilization 37 | averageUtilization: 60 38 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/hpa_service_account.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | enableAutoscaling: true 9 | serviceAccountName: my-service-account 10 | resources: 11 | limits: 12 | cpu: 100m 13 | memory: 128Mi 14 | --- 15 | apiVersion: autoscaling/v2 16 | kind: HorizontalPodAutoscaler 17 | metadata: 18 | name: example-app-autoscaler 19 | spec: 20 | scaleTargetRef: 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | name: example-app 24 | minReplicas: 2 25 | maxReplicas: 3 26 | metrics: 27 | - type: Resource 28 | resource: 29 | name: cpu 30 | target: 31 | type: Utilization 32 | averageUtilization: 60 33 | - type: Resource 34 | resource: 35 | name: memory 36 | target: 37 | type: Utilization 38 | averageUtilization: 60 39 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/keda_autoscaler.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | enableAutoscaling: true 9 | resources: 10 | limits: 11 | cpu: 100m 12 | memory: 128Mi 13 | --- 14 | apiVersion: keda.sh/v1alpha1 15 | kind: ScaledObject 16 | metadata: 17 | name: example-app-autoscaler 18 | spec: 19 | scaleTargetRef: 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | name: example-app 23 | minReplicaCount: 2 24 | maxReplicaCount: 3 25 | triggers: 26 | - type: cpu 27 | metricType: Utilization 28 | metadata: 29 | value: "60" 30 | - type: memory 31 | metricType: Utilization 32 | metadata: 33 | value: "60" 34 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/multiple_image_secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | imagePullSecrets: 10 | - name: secret-name 11 | - name: secret-name-2 12 | runtimeConfig: 13 | loadFromSecret: example-app-runtime-config 14 | --- 15 | apiVersion: v1 16 | kind: Secret 17 | metadata: 18 | name: example-app-runtime-config 19 | type: Opaque 20 | data: 21 | runtime-config.toml: bG9nX2RpciA9ICIvYXNkZiIK 22 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/no_service_account_name.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/one_image_secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | imagePullSecrets: 10 | - name: secret-name 11 | runtimeConfig: 12 | loadFromSecret: example-app-runtime-config 13 | --- 14 | apiVersion: v1 15 | kind: Secret 16 | metadata: 17 | name: example-app-runtime-config 18 | type: Opaque 19 | data: 20 | runtime-config.toml: bG9nX2RpciA9ICIvYXNkZiIK 21 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/runtime-config.toml: -------------------------------------------------------------------------------- 1 | log_dir = "/asdf" 2 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/scaffold_image.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/scaffold_runtime_config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | runtimeConfig: 10 | loadFromSecret: example-app-runtime-config 11 | --- 12 | apiVersion: v1 13 | kind: Secret 14 | metadata: 15 | name: example-app-runtime-config 16 | type: Opaque 17 | data: 18 | runtime-config.toml: bG9nX2RpciA9ICIvYXNkZiIK 19 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/service_account_name.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | serviceAccountName: my-service-account 10 | -------------------------------------------------------------------------------- /pkg/cmd/testdata/variables.yml: -------------------------------------------------------------------------------- 1 | apiVersion: core.spinkube.dev/v1alpha1 2 | kind: SpinApp 3 | metadata: 4 | name: example-app 5 | spec: 6 | image: "ghcr.io/foo/example-app:v0.1.0" 7 | executor: containerd-shim-spin 8 | replicas: 2 9 | variables: 10 | - name: bar 11 | value: yee 12 | - name: foo 13 | value: yoo 14 | -------------------------------------------------------------------------------- /pkg/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | "k8s.io/client-go/discovery" 10 | "sigs.k8s.io/controller-runtime/pkg/client/config" 11 | ) 12 | 13 | // Version is set during build time 14 | var Version = "unknown" 15 | 16 | // actionCmd is the github action command 17 | var versionCmd = &cobra.Command{ 18 | Use: "version", 19 | Short: "Display version information", 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | shortFlag, err := cmd.Flags().GetBool("short") 22 | if err != nil { 23 | log.Fatalf("Error getting short flag: %v", err) 24 | } 25 | 26 | if shortFlag { 27 | fmt.Println(Version) 28 | return 29 | } 30 | 31 | spinVersion := os.Getenv("SPIN_VERSION") 32 | printVersionLine("Plugin Version", Version) 33 | if spinVersion != "" { 34 | printVersionLine("Spin Version", "v"+spinVersion) 35 | } 36 | 37 | serverVersion, err := getServerVersion() 38 | if err != nil { 39 | return 40 | } 41 | printVersionLine("Kubernetes Version", serverVersion) 42 | }, 43 | } 44 | 45 | func printVersionLine(name string, version string) { 46 | fmt.Printf("%-14s: %s\n", name, version) 47 | } 48 | 49 | func getServerVersion() (string, error) { 50 | cfg, err := config.GetConfig() 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | client, err := discovery.NewDiscoveryClientForConfig(cfg) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | serverVersion, err := client.ServerVersion() 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | return serverVersion.String(), nil 66 | } 67 | 68 | func init() { 69 | versionCmd.Flags().BoolP("short", "s", false, "Print only the plugin version") 70 | rootCmd.AddCommand(versionCmd) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/kube/spin.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1" 8 | "k8s.io/cli-runtime/pkg/genericclioptions" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | const FieldManager = "spin-plugin-kube" 13 | 14 | type Impl struct { 15 | kubeclient client.Client 16 | configFlags *genericclioptions.ConfigFlags 17 | } 18 | 19 | func New(kubeclient client.Client, configFlags *genericclioptions.ConfigFlags) *Impl { 20 | return &Impl{ 21 | kubeclient: kubeclient, 22 | configFlags: configFlags, 23 | } 24 | } 25 | 26 | // ListSpinApps returns all resources of type SpinApp in the given namespace. If namespace is the empty string, it 27 | // returns all SpinApp resources across all namespaces. 28 | func (i *Impl) ListSpinApps(ctx context.Context, namespace string) (spinv1alpha1.SpinAppList, error) { 29 | var spinAppList spinv1alpha1.SpinAppList 30 | err := i.kubeclient.List(ctx, &spinAppList, &client.ListOptions{ 31 | Namespace: namespace, 32 | }) 33 | if err != nil { 34 | return spinv1alpha1.SpinAppList{}, err 35 | } 36 | 37 | return spinAppList, nil 38 | } 39 | 40 | func (i *Impl) ApplySpinApp(ctx context.Context, app *spinv1alpha1.SpinApp) error { 41 | patchMethod := client.Apply 42 | patchOptions := &client.PatchOptions{ 43 | Force: ptr(true), 44 | FieldManager: FieldManager, 45 | } 46 | 47 | return i.kubeclient.Patch(ctx, app, patchMethod, patchOptions) 48 | } 49 | 50 | func (i *Impl) GetSpinApp(ctx context.Context, name client.ObjectKey) (spinv1alpha1.SpinApp, error) { 51 | var app spinv1alpha1.SpinApp 52 | err := i.kubeclient.Get(ctx, name, &app) 53 | if err != nil { 54 | return spinv1alpha1.SpinApp{}, err 55 | } 56 | 57 | return app, nil 58 | } 59 | 60 | func (i *Impl) DeleteSpinApp(ctx context.Context, name client.ObjectKey) error { 61 | app, err := i.GetSpinApp(ctx, name) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | fmt.Println("calling delete") 67 | err = i.kubeclient.Delete(ctx, &app) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func ptr[T any](v T) *T { 76 | return &v 77 | } 78 | -------------------------------------------------------------------------------- /release-process.md: -------------------------------------------------------------------------------- 1 | # Cutting a new release of the Spin Kube plugin 2 | 3 | To cut a new release of the Spin Kube plugin, you will need to do the following: 4 | 5 | 1. Confirm that [CI is green](https://github.com/spinframework/spin-trigger-sqs/actions) for the commit selected to be tagged and released. 6 | 7 | 2. Update [`go.mod`](https://github.com/spinframework/spin-plugin-kube/blob/main/go.mod#L13) to ensure that the Spin Operator module is at the desired/latest version. Create a pull request with these changes and merge once approved. 8 | 9 | 3. Change the version number in [spin-pluginify.toml](./spin-pluginify.toml). Create a pull request with these changes and merge once approved. 10 | 11 | 4. Checkout the commit with the version bump from above. 12 | 13 | 5. Create and push a new tag with a `v` and then the version number. 14 | 15 | As an example, via the `git` CLI: 16 | 17 | ```console 18 | # Create a GPG-signed and annotated tag 19 | git tag -s -m "Spin Kube Plugin v0.4.0" v0.4.0 20 | 21 | # Push the tag to the remote corresponding to spinframework/spin-kube-plugin (here 'origin') 22 | git push origin v0.4.0 23 | ``` 24 | 25 | 6. Pushing the tag upstream will trigger the [release action](https://github.com/spinframework/spin-plugin-kube/blob/main/.github/workflows/release.yml). 26 | - The release build will create the packaged versions of the plugin, the updated plugin manifest and a checksums file 27 | - These assets are uploaded to a new GitHub release for the pushed tag 28 | - Release notes are auto-generated but edit as needed especially around breaking changes or other notable items 29 | 30 | 7. Validate that CI created a PR in the [fermyon/spin-plugins](https://github.com/fermyon/spin-plugins) repository with the [updated manifest](https://github.com/fermyon/spin-plugins/tree/main/manifests/kube). 31 | 32 | 8. If applicable, create PR(s) or coordinate [documentation](https://github.com/spinframework/spinkube-docs) needs, e.g. for new features or updated functionality. 33 | -------------------------------------------------------------------------------- /spin-pluginify.toml: -------------------------------------------------------------------------------- 1 | name = "kube" 2 | description = "Commands for publishing applications to Kubernetes using the spin-operator." 3 | version = "0.4.0" 4 | spin_compatibility = ">=2.3.1" 5 | license = "Apache-2.0" 6 | homepage = "https://github.com/spinkube/spin-plugin-kube" 7 | package = "./bin/spin-kube" 8 | --------------------------------------------------------------------------------