├── testdata ├── node-installer │ ├── distros │ │ ├── unsupported │ │ │ └── .gitkeep │ │ ├── k0s │ │ │ └── etc │ │ │ │ └── k0s │ │ │ │ └── containerd.toml │ │ ├── default │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ ├── microk8s │ │ │ └── var │ │ │ │ └── snap │ │ │ │ └── microk8s │ │ │ │ └── current │ │ │ │ └── args │ │ │ │ └── containerd-template.toml │ │ ├── k3s │ │ │ └── var │ │ │ │ └── lib │ │ │ │ └── rancher │ │ │ │ └── k3s │ │ │ │ └── agent │ │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ └── rke2 │ │ │ └── var │ │ │ └── lib │ │ │ └── rancher │ │ │ └── rke2 │ │ │ └── agent │ │ │ └── etc │ │ │ └── containerd │ │ │ └── config.toml │ ├── containerd │ │ ├── 1.x │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ ├── 2.x │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ ├── default-and-k0s-configs │ │ │ └── etc │ │ │ │ ├── k0s │ │ │ │ └── containerd.toml │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ ├── missing-containerd-config │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── .gitkeep │ │ ├── rke2-only-base-config-exists │ │ │ └── var │ │ │ │ └── lib │ │ │ │ └── rancher │ │ │ │ └── rke2 │ │ │ │ └── agent │ │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ ├── rke2-existing-config-tmpl │ │ │ └── var │ │ │ │ └── lib │ │ │ │ └── rancher │ │ │ │ └── rke2 │ │ │ │ └── agent │ │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml.tmpl │ │ ├── existing-containerd-shim-config │ │ │ ├── opt │ │ │ │ └── rcm │ │ │ │ │ └── rcm-lock.json │ │ │ └── etc │ │ │ │ └── containerd │ │ │ │ └── config.toml │ │ └── missing-containerd-shim-config │ │ │ └── etc │ │ │ └── containerd │ │ │ └── config.toml │ ├── assets │ │ ├── containerd-shim-spin-v1 │ │ └── containerd-shim-slight-v1 │ ├── shim │ │ └── opt │ │ │ └── rcm │ │ │ └── rcm-lock.json │ └── shim-missing-binary │ │ └── opt │ │ └── rcm │ │ └── rcm-lock.json └── apps │ └── spin-app.yaml ├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── samples │ ├── kustomization.yaml │ ├── test_shim_wws.yaml │ ├── test_shim_slight.yaml │ ├── test_shim_lunatic.yaml │ └── test_shim_spin.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── shim_viewer_role.yaml │ ├── kustomization.yaml │ ├── shim_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml └── crd │ ├── kustomizeconfig.yaml │ └── kustomization.yaml ├── example └── node-installer │ ├── runtimeclass.yaml │ ├── test-job.yaml │ ├── install-job.yaml │ ├── debug.yaml │ └── daemonset.yaml ├── Dockerfile.dockerignore ├── tilt-settings.yaml.example ├── .dockerignore ├── tilt.dockerfile ├── images ├── downloader │ ├── Dockerfile │ └── download_shim.sh ├── installer │ └── Dockerfile └── test │ └── Dockerfile.minikube-custom ├── internal ├── containerd │ ├── restart_windows.go │ ├── restart_unix_test.go │ ├── install_dbus.go │ ├── configure.go │ └── restart_unix.go ├── shim │ ├── uninstall.go │ ├── shim.go │ ├── install.go │ ├── uninstall_test.go │ └── install_test.go ├── state │ ├── shim.go │ ├── state.go │ ├── shim_test.go │ └── state_test.go ├── preset │ ├── preset_test.go │ └── preset.go └── controller │ ├── suite_test.go │ └── job_controller.go ├── hack ├── kind.yaml └── boilerplate.go.txt ├── docs ├── shim_delete.md ├── runtimeclass.md └── supported_distros.md ├── deploy ├── helm │ ├── templates │ │ ├── clusterrolebinding.yaml │ │ ├── serviceaccount.yaml │ │ ├── NOTES.txt │ │ ├── clusterrole.yaml │ │ ├── _helpers.tpl │ │ └── deployment.yaml │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ └── values.yaml └── update-chart-versions.sh ├── .gitignore ├── .vscode └── launch.json ├── .github ├── pull_request_template.md ├── workflows │ ├── sign-image.yml │ ├── manager-build.yml │ ├── installer-build.yml │ ├── ci.yml │ ├── downloader-build.yml │ ├── release-drafter.yml │ ├── dependency-review.yml │ ├── helm-chart-release.yml │ ├── sbom.yml │ ├── scorecard.yml │ ├── container-image.yml │ ├── release.yml │ └── helm-chart-node-scaling-test.yml ├── dependabot.yml └── release-drafter.yml ├── cmd ├── node-installer │ ├── main.go │ ├── config.go │ ├── detect.go │ ├── uninstall.go │ ├── root.go │ ├── install_test.go │ ├── install.go │ └── detect_test.go └── rcm │ └── main.go ├── SECURITY.md ├── tests └── node-installer │ └── fs.go ├── PROJECT ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── shim_types.go │ └── zz_generated.deepcopy.go ├── Dockerfile ├── CONTRIBUTING.md ├── Tiltfile ├── go.mod ├── README.md └── .golangci.yaml /testdata/node-installer/distros/unsupported/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/distros/k0s/etc/k0s/containerd.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/distros/default/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/1.x/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | version = 2 -------------------------------------------------------------------------------- /testdata/node-installer/containerd/2.x/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | version = 3 -------------------------------------------------------------------------------- /testdata/node-installer/containerd/default-and-k0s-configs/etc/k0s/containerd.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/default-and-k0s-configs/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/missing-containerd-config/etc/containerd/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/distros/microk8s/var/snap/microk8s/current/args/containerd-template.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/node-installer/distros/k3s/var/lib/rancher/k3s/agent/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | version = 2 -------------------------------------------------------------------------------- /testdata/node-installer/distros/rke2/var/lib/rancher/rke2/agent/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | version = 2 -------------------------------------------------------------------------------- /testdata/node-installer/assets/containerd-shim-spin-v1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "Hello from spin shim" -------------------------------------------------------------------------------- /testdata/node-installer/assets/containerd-shim-slight-v1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "hello from spin-v1" -------------------------------------------------------------------------------- /testdata/node-installer/containerd/rke2-only-base-config-exists/var/lib/rancher/rke2/agent/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | version = 2 -------------------------------------------------------------------------------- /example/node-installer/runtimeclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: node.k8s.io/v1 2 | kind: RuntimeClass 3 | metadata: 4 | name: crun 5 | handler: crun 6 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/rke2-existing-config-tmpl/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl: -------------------------------------------------------------------------------- 1 | version = 2 2 | preexisting-config = true -------------------------------------------------------------------------------- /Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /tilt-settings.yaml.example: -------------------------------------------------------------------------------- 1 | # Replace with a development registry that offers pull & push access 2 | registry: ghcr.io//runtime-class-manager 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | example 6 | images 7 | -------------------------------------------------------------------------------- /tilt.dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.23.0@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 2 | WORKDIR / 3 | COPY ./bin/manager /manager 4 | 5 | ENTRYPOINT ["/manager"] 6 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: controller 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - test_shim_lunatic.yaml 4 | - test_shim_slight.yaml 5 | - test_shim_spin.yaml 6 | - test_shim_wws.yaml 7 | #+kubebuilder:scaffold:manifestskustomizesamples 8 | -------------------------------------------------------------------------------- /images/downloader/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.23.0@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 2 | 3 | RUN apk add --no-cache curl bash tar 4 | COPY download_shim.sh /download_shim.sh 5 | CMD ["bash", "/download_shim.sh" ] 6 | -------------------------------------------------------------------------------- /testdata/node-installer/shim/opt/rcm/rcm-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "shims": { 3 | "spin-v1": { 4 | "sha256": "6da5e8f17a9bfa9cb04cf22c87b6475394ecec3af4fdc337f72d6dbf3319ea52", 5 | "path": "/opt/rcm/bin/containerd-shim-spin-v1" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/node-installer/shim-missing-binary/opt/rcm/rcm-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "shims": { 3 | "spin-v1": { 4 | "sha256": "6da5e8f17a9bfa9cb04cf22c87b6475394ecec3af4fdc337f72d6dbf3319ea52", 5 | "path": "/opt/rcm/bin/containerd-shim-spin-v1" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/existing-containerd-shim-config/opt/rcm/rcm-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "shims": { 3 | "spin-v1": { 4 | "sha256": "6da5e8f17a9bfa9cb04cf22c87b6475394ecec3af4fdc337f72d6dbf3319ea52", 5 | "path": "/opt/rcm/bin/containerd-shim-spin-v1" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internal/containerd/restart_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package containerd 5 | 6 | import "errors" 7 | 8 | type ContainerdRestarter struct{} 9 | 10 | func (r ContainerdRestarter) Restart() error { 11 | return errors.New("restarting containerd not implemented") 12 | } 13 | -------------------------------------------------------------------------------- /hack/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | labels: 7 | spin: true 8 | - role: worker 9 | labels: 10 | wws: true 11 | spin: true 12 | - role: worker 13 | labels: 14 | slight: true 15 | - role: worker 16 | labels: 17 | lunatic: true 18 | -------------------------------------------------------------------------------- /docs/shim_delete.md: -------------------------------------------------------------------------------- 1 | Deleting a Shim 2 | 3 | 4 | - if a shim is deleted, an "uninstall"-job is scheduled on every node that matched the shim's selector 5 | 6 | 7 | What happens if an uninstall job does not complete (successfully)? 8 | 9 | - the deletion of the shim must not be blocked by uninstall-jobs 10 | - node should still be annotated with "uninstall" or "failed" 11 | -------------------------------------------------------------------------------- /deploy/helm/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | creationTimestamp: null 5 | name: {{ include "rcm.fullname" . }} 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: {{ include "rcm.fullname" . }} 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ include "rcm.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /deploy/helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "rcm.serviceAccountName" . }} 6 | labels: 7 | {{- include "rcm.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | Dockerfile.cross 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "./cmd/rcm/main.go" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Issue ticket number and link 4 | 5 | ## Checklist before requesting a review 6 | - [ ] I have performed a self-review of my code 7 | - [ ] If it is a core feature, I have added thorough tests. 8 | - I tested the changes with the following distributions: 9 | - [ ] Kind 10 | - [ ] MiniKube 11 | - [ ] MicroK8s 12 | - [ ] Rancher RKE2 13 | - [ ] Azure AKS 14 | - [ ] GCP GKE (Ubuntu nodes) 15 | - [ ] AWS EKS (AmazonLinux2 nodes) 16 | - [ ] AWS EKS (Ubuntu nodes) 17 | - [ ] Digital Ocean Kubernetes -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /example/node-installer/test-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: node.k8s.io/v1 2 | kind: RuntimeClass 3 | metadata: 4 | name: wasmedge 5 | handler: wasmedge 6 | --- 7 | apiVersion: batch/v1 8 | kind: Job 9 | metadata: 10 | creationTimestamp: null 11 | name: wasm-test 12 | spec: 13 | template: 14 | metadata: 15 | annotations: 16 | module.wasm.image/variant: compat-smart 17 | creationTimestamp: null 18 | spec: 19 | containers: 20 | - image: wasmedge/example-wasi:latest 21 | name: wasm-test 22 | resources: {} 23 | restartPolicy: Never 24 | runtimeClassName: wasmedge 25 | backoffLimit: 1 26 | -------------------------------------------------------------------------------- /images/installer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25@sha256:a22b2e6c5e753345b9759fba9e5c1731ebe28af506745e98f406cc85d50c828e AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | 10 | RUN CGO_ENABLED=0 go build -o rcm-node-installer ./cmd/node-installer 11 | RUN /app/rcm-node-installer -h 12 | 13 | # Using busybox instead of scratch so that the nsenter utility is present, as used in restarter logic 14 | FROM busybox:1.37@sha256:d80cd694d3e9467884fcb94b8ca1e20437d8a501096cdf367a5a1918a34fc2fd 15 | COPY --from=builder /app/rcm-node-installer /rcm-node-installer 16 | 17 | ENTRYPOINT ["/rcm-node-installer"] 18 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /docs/runtimeclass.md: -------------------------------------------------------------------------------- 1 | ## RuntimeClass 2 | 3 | The Operator is designed to create a RuntimeClass for each shim. `spec.runtimeClass` configures the RuntimeClass that will be created. 4 | 5 | * `spec.runtimeClass.name`: Name of the Kubernetes RuntimeClass 6 | * `spec.runtimeClass.handler`: Name of the shim as it is referenced in the containerd config 7 | 8 | **Discuss later:** 9 | 10 | - At this point in time `spec.RuntimeClass` is a mendatory field 11 | - pro: it will make sure a RuntimeClass exist for the shim thats going to be installed 12 | - con: possible that runtimeclass is created by other means 13 | - Should `spec.RuntimeClass.handler` be optional? Is it even required? 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/missing-containerd-shim-config/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | [plugins] 2 | [plugins."io.containerd.monitor.v1.cgroups"] 3 | no_prometheus = false 4 | [plugins."io.containerd.service.v1.diff-service"] 5 | default = ["walking"] 6 | [plugins."io.containerd.gc.v1.scheduler"] 7 | pause_threshold = 0.02 8 | deletion_threshold = 0 9 | mutation_threshold = 100 10 | schedule_delay = 0 11 | startup_delay = "100ms" 12 | [plugins."io.containerd.runtime.v2.task"] 13 | platforms = ["linux/amd64"] 14 | sched_core = true 15 | [plugins."io.containerd.service.v1.tasks-service"] 16 | blockio_config_file = "" 17 | rdt_config_file = "" 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: runtime-class-manager 9 | app.kubernetes.io/part-of: runtime-class-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: runtime-class-manager 10 | app.kubernetes.io/part-of: runtime-class-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /testdata/apps/spin-app.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: wasm-spin 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: wasm-spin 11 | template: 12 | metadata: 13 | labels: 14 | app: wasm-spin 15 | spec: 16 | runtimeClassName: wasmtime-spin-v2 17 | containers: 18 | - name: spin-hello 19 | image: ghcr.io/spinframework/spin-operator/hello-world:20250206-205102-gf007281 20 | command: ["/"] 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: wasm-spin 26 | spec: 27 | ports: 28 | - protocol: TCP 29 | port: 80 30 | targetPort: 80 31 | selector: 32 | app: wasm-spin 33 | -------------------------------------------------------------------------------- /cmd/node-installer/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | func main() { 20 | Execute() 21 | } 22 | -------------------------------------------------------------------------------- /config/rbac/shim_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view shims. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: shim-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: runtime-class-manager 10 | app.kubernetes.io/part-of: runtime-class-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: shim-viewer-role 13 | rules: 14 | - apiGroups: 15 | - runtime.spinkube.dev 16 | resources: 17 | - shims 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - runtime.spinkube.dev 24 | resources: 25 | - shims/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/samples/test_shim_wws.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: runtime.spinkube.dev/v1alpha1 2 | kind: Shim 3 | metadata: 4 | name: wws-v1 5 | labels: 6 | app.kubernetes.io/name: wws-v1 7 | app.kubernetes.io/instance: wws-v1 8 | app.kubernetes.io/part-of: runtime-class-manager 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: runtime-class-manager 11 | spec: 12 | nodeSelector: 13 | wws: "true" 14 | 15 | fetchStrategy: 16 | type: anonymousHttp 17 | anonHttp: 18 | location: "https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.10.0/containerd-wasm-shims-v1-wws-linux-aarch64.tar.gz" 19 | 20 | runtimeClass: 21 | name: wws-v1 22 | handler: wws 23 | 24 | rolloutStrategy: 25 | type: recreate 26 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/samples/test_shim_slight.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: runtime.spinkube.dev/v1alpha1 2 | kind: Shim 3 | metadata: 4 | name: slight-v1 5 | labels: 6 | app.kubernetes.io/name: slight-v1 7 | app.kubernetes.io/instance: slight-v1 8 | app.kubernetes.io/part-of: runtime-class-manager 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: runtime-class-manager 11 | spec: 12 | nodeSelector: 13 | slight: "true" 14 | 15 | fetchStrategy: 16 | type: anonymousHttp 17 | anonHttp: 18 | location: "https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.10.0/containerd-wasm-shims-v1-slight-linux-aarch64.tar.gz" 19 | 20 | runtimeClass: 21 | name: slight-v1 22 | handler: slight 23 | 24 | rolloutStrategy: 25 | type: recreate 26 | -------------------------------------------------------------------------------- /config/samples/test_shim_lunatic.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: runtime.spinkube.dev/v1alpha1 2 | kind: Shim 3 | metadata: 4 | name: lunatic-v1 5 | labels: 6 | app.kubernetes.io/name: lunatic-v1 7 | app.kubernetes.io/instance: lunatic-v1 8 | app.kubernetes.io/part-of: runtime-class-manager 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: runtime-class-manager 11 | spec: 12 | nodeSelector: 13 | lunatic: "true" 14 | 15 | fetchStrategy: 16 | type: anonymousHttp 17 | anonHttp: 18 | location: "https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.10.0/containerd-wasm-shims-v1-lunatic-linux-aarch64.tar.gz" 19 | 20 | runtimeClass: 21 | name: lunatic-v1 22 | handler: lunatic 23 | 24 | rolloutStrategy: 25 | type: recreate 26 | -------------------------------------------------------------------------------- /config/rbac/shim_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit shims. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: shim-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: runtime-class-manager 10 | app.kubernetes.io/part-of: runtime-class-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: shim-editor-role 13 | rules: 14 | - apiGroups: 15 | - runtime.spinkube.dev 16 | resources: 17 | - shims 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - runtime.spinkube.dev 28 | resources: 29 | - shims/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting a Vulnerability 4 | 5 | The Spin Framework team and community take security vulnerabilities very seriously. If you find a security related issue with Runtime Class Manager, we kindly ask you to report it [through the GitHub project](https://github.com/spinframework/runtime-class-manager/security). All reports will be thoroughly investigated by the Spin Framework maintainers. 6 | 7 | ## Disclosure 8 | 9 | We will disclose any security vulnerabilities in our [Security Advisories](https://github.com/spinframework/runtime-class-manager/security/advisories). 10 | 11 | All vulnerabilities and associated information will be treated with full confidentiality. We are thankful for your efforts in keeping Runtime Class Manager secure, and we will publicly acknowledge your contributions if you wish. -------------------------------------------------------------------------------- /internal/shim/uninstall.go: -------------------------------------------------------------------------------- 1 | package shim 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spinframework/runtime-class-manager/internal/state" 9 | ) 10 | 11 | func (c *Config) Uninstall(shimName string) (string, error) { 12 | st, err := state.Get(c.hostFs, c.rcmPath) 13 | if err != nil { 14 | return "", err 15 | } 16 | s, ok := st.Shims[shimName] 17 | if !ok { 18 | return "", fmt.Errorf("shim %s not installed", shimName) 19 | } 20 | filePath := s.Path 21 | 22 | err = c.hostFs.Remove(filePath) 23 | if err != nil { 24 | if !errors.Is(err, os.ErrNotExist) { 25 | return "", fmt.Errorf("shim binary at %s does not exist, nothing to delete", filePath) 26 | } 27 | } 28 | st.RemoveShim(shimName) 29 | if err = st.Write(); err != nil { 30 | return "", err 31 | } 32 | return filePath, err 33 | } 34 | -------------------------------------------------------------------------------- /internal/state/shim.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | ) 7 | 8 | type Shim struct { 9 | Sha256 []byte 10 | Path string 11 | } 12 | 13 | func (s *Shim) MarshalJSON() ([]byte, error) { 14 | return json.Marshal(&struct { 15 | Sha256 string `json:"sha256"` 16 | Path string `json:"path"` 17 | }{ 18 | Sha256: hex.EncodeToString(s.Sha256), 19 | Path: s.Path, 20 | }) 21 | } 22 | 23 | func (s *Shim) UnmarshalJSON(data []byte) error { 24 | var aux struct { 25 | Sha256 string `json:"sha256"` 26 | Path string `json:"path"` 27 | } 28 | if err := json.Unmarshal(data, &aux); err != nil { 29 | return err 30 | } 31 | s.Path = aux.Path 32 | sha256, err := hex.DecodeString(aux.Sha256) 33 | if err != nil { 34 | return err 35 | } 36 | s.Sha256 = sha256 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /tests/node-installer/fs.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | func FixtureFs(fixturePath string) afero.Fs { 11 | baseFs := afero.NewBasePathFs(afero.NewOsFs(), fixturePath) 12 | fs := afero.NewMemMapFs() 13 | err := afero.Walk(baseFs, "/", func(path string, info os.FileInfo, err error) error { 14 | if err != nil { 15 | return err 16 | } 17 | if info.IsDir() { 18 | return fs.MkdirAll(path, 0755) //nolint:mnd // file permissions 19 | } 20 | src, err := baseFs.Open(path) 21 | if err != nil { 22 | return err 23 | } 24 | dst, err := fs.Create(path) 25 | if err != nil { 26 | return err 27 | } 28 | _, err = io.Copy(dst, src) 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | }) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return fs 38 | } 39 | -------------------------------------------------------------------------------- /testdata/node-installer/containerd/existing-containerd-shim-config/etc/containerd/config.toml: -------------------------------------------------------------------------------- 1 | [plugins] 2 | [plugins."io.containerd.monitor.v1.cgroups"] 3 | no_prometheus = false 4 | [plugins."io.containerd.service.v1.diff-service"] 5 | default = ["walking"] 6 | [plugins."io.containerd.gc.v1.scheduler"] 7 | pause_threshold = 0.02 8 | deletion_threshold = 0 9 | mutation_threshold = 100 10 | schedule_delay = 0 11 | startup_delay = "100ms" 12 | [plugins."io.containerd.runtime.v2.task"] 13 | platforms = ["linux/amd64"] 14 | sched_core = true 15 | [plugins."io.containerd.service.v1.tasks-service"] 16 | blockio_config_file = "" 17 | rdt_config_file = "" 18 | 19 | # RCM runtime config for spin-v1 20 | [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.spin-v1] 21 | runtime_type = "/opt/rcm/bin/containerd-shim-spin-v1" 22 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: spinkube.dev 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: runtime-class-manager 9 | repo: github.com/spinframework/runtime-class-manager 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: spinkube.dev 16 | group: runtime 17 | kind: Shim 18 | path: github.com/spinframework/runtime-class-manager/api/v1alpha1 19 | version: v1alpha1 20 | - controller: true 21 | domain: spinkube.dev 22 | group: runtime 23 | kind: Node 24 | version: v1alpha1 25 | - controller: true 26 | domain: spinkube.dev 27 | group: runtime 28 | kind: Job 29 | version: v1alpha1 30 | version: "3" 31 | -------------------------------------------------------------------------------- /deploy/helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Welcome to Runtime-Class-Manager. 2 | 3 | Next steps: 4 | 5 | Create one or more Wasm Shim custom resources. See the samples in https://github.com/spinframework/runtime-class-manager/tree/main/config/samples. 6 | 7 | > Note: Ensure that the `location` for the specified shim binary points to the correct architecture for your Node(s) 8 | 9 | For example, install the Spin shim: 10 | 11 | ```shell 12 | kubectl apply -f https://raw.githubusercontent.com/spinframework/runtime-class-manager/refs/heads/main/config/samples/test_shim_spin.yaml 13 | ``` 14 | 15 | Next, annotate one or more nodes with a label corresponding to the `nodeSelector` declared in the Shim, runtime-class-manager will install the shim as well as create the corresponding RuntimeClass: 16 | 17 | ```shell 18 | kubectl label node --all spin=true 19 | ``` 20 | 21 | You are now ready to deploy your Wasm workloads. 22 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: servicemonitor 8 | app.kubernetes.io/instance: controller-manager-metrics-monitor 9 | app.kubernetes.io/component: metrics 10 | app.kubernetes.io/created-by: runtime-class-manager 11 | app.kubernetes.io/part-of: runtime-class-manager 12 | app.kubernetes.io/managed-by: kustomize 13 | name: controller-manager-metrics-monitor 14 | namespace: system 15 | spec: 16 | endpoints: 17 | - path: /metrics 18 | port: https 19 | scheme: https 20 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 21 | tlsConfig: 22 | insecureSkipVerify: true 23 | selector: 24 | matchLabels: 25 | control-plane: controller-manager 26 | -------------------------------------------------------------------------------- /example/node-installer/install-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | creationTimestamp: null 5 | name: default-init 6 | spec: 7 | template: 8 | metadata: 9 | labels: 10 | name: kwasm-initializer 11 | app: default-init 12 | spec: 13 | restartPolicy: Never 14 | hostPID: true 15 | volumes: 16 | - name: node-root 17 | hostPath: 18 | path: / 19 | - name: entrypoint 20 | configMap: 21 | name: entrypoint 22 | defaultMode: 0744 23 | containers: 24 | - image: ghcr.io/kwasm/kwasm-node-installer:master 25 | name: kwasm-initializer 26 | env: 27 | - name: NODE_ROOT 28 | value: /mnt/node-root 29 | securityContext: 30 | privileged: true 31 | volumeMounts: 32 | - name: node-root 33 | mountPath: /mnt/node-root/ 34 | backoffLimit: 1 35 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: runtime-class-manager 10 | app.kubernetes.io/part-of: runtime-class-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - batch 9 | resources: 10 | - jobs 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - batch 21 | resources: 22 | - jobs/finalizers 23 | verbs: 24 | - update 25 | - apiGroups: 26 | - batch 27 | resources: 28 | - jobs/status 29 | verbs: 30 | - get 31 | - patch 32 | - update 33 | - apiGroups: 34 | - runtime.spinkube.dev 35 | resources: 36 | - shims 37 | verbs: 38 | - create 39 | - delete 40 | - get 41 | - list 42 | - patch 43 | - update 44 | - watch 45 | - apiGroups: 46 | - runtime.spinkube.dev 47 | resources: 48 | - shims/finalizers 49 | verbs: 50 | - update 51 | - apiGroups: 52 | - runtime.spinkube.dev 53 | resources: 54 | - shims/status 55 | verbs: 56 | - get 57 | - patch 58 | - update 59 | -------------------------------------------------------------------------------- /deploy/helm/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: {{ include "rcm.fullname" . }} 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - nodes 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - update 16 | 17 | # TODO: It seems like runtime-class-manger should only need to modify jobs in its own namespace, 18 | # i.e. via a namespaced Role. However, RBAC errors result without these clusterrole permissions. 19 | - apiGroups: 20 | - batch 21 | resources: 22 | - jobs 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - delete 29 | - patch 30 | 31 | - apiGroups: 32 | - runtime.spinkube.dev 33 | resources: 34 | - shims 35 | verbs: 36 | - get 37 | - list 38 | - watch 39 | - update 40 | 41 | - apiGroups: 42 | - node.k8s.io 43 | resources: 44 | - runtimeclasses 45 | verbs: 46 | - get 47 | - list 48 | - watch 49 | - create 50 | - delete 51 | - patch 52 | -------------------------------------------------------------------------------- /.github/workflows/sign-image.yml: -------------------------------------------------------------------------------- 1 | name: Sign image 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | image-repository: 9 | type: string 10 | required: true 11 | image-digest: 12 | type: string 13 | required: true 14 | 15 | jobs: 16 | sign: 17 | name: Sign image 18 | permissions: 19 | packages: write 20 | id-token: write 21 | 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Install cosign 25 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 26 | 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Sign container image 35 | run: | 36 | cosign sign --yes \ 37 | ${{ inputs.image-repository }}@${{ inputs.image-digest }} 38 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/runtime.spinkube.dev_shims.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- path: patches/webhook_in_shims.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- path: patches/cainjection_in_shims.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # [WEBHOOK] To enable webhook, uncomment the following section 20 | # the following config is for teaching kustomize how to do kustomization for CRDs. 21 | 22 | #configurations: 23 | #- kustomizeconfig.yaml 24 | -------------------------------------------------------------------------------- /example/node-installer/debug.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: kwasm-debug 5 | labels: 6 | app: default 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: default 11 | updateStrategy: 12 | type: RollingUpdate 13 | template: 14 | metadata: 15 | labels: 16 | name: kwasm-debug 17 | app: default 18 | spec: 19 | hostPID: true 20 | volumes: 21 | - name: node-root 22 | hostPath: 23 | path: / 24 | - name: entrypoint 25 | configMap: 26 | name: entrypoint 27 | defaultMode: 0744 28 | containers: 29 | - image: ubuntu 30 | name: kwasm-debug 31 | command: 32 | - sh 33 | - -c 34 | - "tail -f /dev/null" 35 | env: 36 | - name: NODE_ROOT 37 | value: /mnt/node-root 38 | securityContext: 39 | privileged: true 40 | volumeMounts: 41 | - name: node-root 42 | mountPath: /mnt/node-root/ 43 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: 8 | - "area/dependencies" 9 | - package-ecosystem: docker 10 | directory: "/images/installer" 11 | schedule: 12 | interval: "weekly" 13 | labels: 14 | - "area/dependencies" 15 | - package-ecosystem: docker 16 | directory: "/images/downloader" 17 | schedule: 18 | interval: "weekly" 19 | labels: 20 | - "area/dependencies" 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | labels: 26 | - "area/dependencies" 27 | - package-ecosystem: "gomod" 28 | directory: "/" 29 | schedule: 30 | interval: "daily" 31 | labels: 32 | - "area/dependencies" 33 | groups: 34 | k8s: 35 | patterns: 36 | - "k8s.io/api" 37 | - "k8s.io/apimachinery" 38 | - "k8s.io/client-go" 39 | - "sigs.k8s.io/controller-runtime" 40 | ginkgo: 41 | patterns: 42 | - "github.com/onsi/ginkgo/v2" 43 | - "github.com/onsi/gomega" 44 | -------------------------------------------------------------------------------- /cmd/node-installer/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | type Config struct { 20 | Runtime struct { 21 | Name string 22 | ConfigPath string 23 | // Options is a map of containerd runtime options for the shim plugin. 24 | // See an example of the cgroup drive option here: 25 | // https://github.com/containerd/containerd/blob/main/docs/cri/config.md#cgroup-driver 26 | Options map[string]string 27 | } 28 | RCM struct { 29 | Path string 30 | AssetPath string 31 | } 32 | Host struct { 33 | RootPath string 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/manager-build.yml: -------------------------------------------------------------------------------- 1 | name: Build manager image, sign it, and generate SBOMs 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | outputs: 9 | digest: 10 | description: "Container image digest" 11 | value: ${{jobs.build.outputs.digest}} 12 | 13 | push: 14 | branches: 15 | - "main" 16 | - "feat-**" 17 | 18 | jobs: 19 | build: 20 | uses: ./.github/workflows/container-image.yml 21 | permissions: 22 | contents: read 23 | packages: write 24 | with: 25 | image-name: runtime-class-manager 26 | dockerfile: ./Dockerfile 27 | push-image: true 28 | 29 | sign: 30 | needs: build 31 | uses: ./.github/workflows/sign-image.yml 32 | permissions: 33 | packages: write 34 | id-token: write 35 | with: 36 | image-repository: ${{ needs.build.outputs.repository }} 37 | image-digest: ${{ needs.build.outputs.digest }} 38 | 39 | sbom: 40 | needs: build 41 | uses: ./.github/workflows/sbom.yml 42 | permissions: 43 | packages: write 44 | id-token: write 45 | with: 46 | image-name: runtime-class-manager 47 | image-digest: ${{ needs.build.outputs.digest }} 48 | -------------------------------------------------------------------------------- /.github/workflows/installer-build.yml: -------------------------------------------------------------------------------- 1 | name: Build installer image, sign it, and generate SBOMs 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | outputs: 9 | digest: 10 | description: "Container image digest" 11 | value: ${{jobs.build.outputs.digest}} 12 | 13 | push: 14 | branches: 15 | - "main" 16 | - "feat-**" 17 | 18 | jobs: 19 | build: 20 | uses: ./.github/workflows/container-image.yml 21 | permissions: 22 | contents: read 23 | packages: write 24 | with: 25 | image-name: node-installer 26 | dockerfile: ./images/installer/Dockerfile 27 | push-image: true 28 | 29 | sign: 30 | needs: build 31 | uses: ./.github/workflows/sign-image.yml 32 | permissions: 33 | packages: write 34 | id-token: write 35 | with: 36 | image-repository: ${{ needs.build.outputs.repository }} 37 | image-digest: ${{ needs.build.outputs.digest }} 38 | 39 | sbom: 40 | needs: build 41 | uses: ./.github/workflows/sbom.yml 42 | permissions: 43 | packages: write 44 | id-token: write 45 | with: 46 | image-name: node-installer 47 | image-digest: ${{ needs.build.outputs.digest }} 48 | -------------------------------------------------------------------------------- /internal/shim/shim.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shim 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/spf13/afero" 23 | ) 24 | 25 | type Config struct { 26 | rootFs afero.Fs 27 | hostFs afero.Fs 28 | assetPath string 29 | rcmPath string 30 | } 31 | 32 | func NewConfig(rootFs afero.Fs, hostFs afero.Fs, assetPath string, rcmPath string) *Config { 33 | return &Config{ 34 | rootFs: rootFs, 35 | hostFs: hostFs, 36 | assetPath: assetPath, 37 | rcmPath: rcmPath, 38 | } 39 | } 40 | 41 | func RuntimeName(bin string) string { 42 | return strings.TrimPrefix(bin, "containerd-shim-") 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_call: 5 | push: 6 | branches: 7 | - "main" 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | helm_install_smoke_test: 15 | uses: ./.github/workflows/helm-chart-smoketest.yml 16 | 17 | helm_node_scaling_test: 18 | uses: ./.github/workflows/helm-chart-node-scaling-test.yml 19 | 20 | unit_tests: 21 | name: Test 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 26 | with: 27 | go-version: "1.25.5" 28 | - run: make test 29 | 30 | golangci: 31 | name: Golangci-lint 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 35 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 36 | with: 37 | go-version: "1.25.5" 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 40 | with: 41 | version: v2.5.0 42 | skip-cache: true 43 | -------------------------------------------------------------------------------- /images/downloader/download_shim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | declare -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3) 5 | script_logging_level="INFO" 6 | 7 | log() { 8 | local log_message=$1 9 | local log_priority=$2 10 | 11 | #check if level exists 12 | [[ ${levels[$log_priority]} ]] || return 1 13 | 14 | #check if level is enough 15 | (( ${levels[$log_priority]} < ${levels[$script_logging_level]} )) && return 2 16 | 17 | #log here 18 | d=$(date '+%Y-%m-%dT%H:%M:%S') 19 | echo -e "${d}\t${log_priority}\t${log_message}" 20 | } 21 | 22 | log "start downloading shim from ${SHIM_LOCATION}..." "INFO" 23 | 24 | mkdir -p /assets 25 | 26 | # overwrite default name of shim binary; use the name of shim resource instead 27 | # to enable installing multiple versions of the same shim 28 | curl -sLo "containerd-shim-${SHIM_NAME}" "${SHIM_LOCATION}" 29 | ls -lah "containerd-shim-${SHIM_NAME}" 30 | 31 | log "$(curl --version)" "INFO" 32 | log "$(tar --version)" "INFO" 33 | log "md5sum: $(md5sum containerd-shim-${SHIM_NAME})" "INFO" 34 | log "sha256sum: $(sha256sum containerd-shim-${SHIM_NAME})" "INFO" 35 | 36 | tar -xzf "containerd-shim-${SHIM_NAME}" -C /assets 37 | log "download successful:" "INFO" 38 | 39 | ls -lah /assets 40 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /.github/workflows/downloader-build.yml: -------------------------------------------------------------------------------- 1 | name: Build shim-downloader image, sign it, and generate SBOMs 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | outputs: 9 | digest: 10 | description: "Container image digest" 11 | value: ${{jobs.build.outputs.digest}} 12 | 13 | push: 14 | branches: 15 | - "main" 16 | - "feat-**" 17 | 18 | jobs: 19 | build: 20 | uses: ./.github/workflows/container-image.yml 21 | permissions: 22 | contents: read 23 | packages: write 24 | with: 25 | image-name: shim-downloader 26 | dockerfile: ./images/downloader/Dockerfile 27 | docker-context: ./images/downloader 28 | push-image: true 29 | 30 | sign: 31 | needs: build 32 | uses: ./.github/workflows/sign-image.yml 33 | permissions: 34 | packages: write 35 | id-token: write 36 | with: 37 | image-repository: ${{ needs.build.outputs.repository }} 38 | image-digest: ${{ needs.build.outputs.digest }} 39 | 40 | sbom: 41 | needs: build 42 | uses: ./.github/workflows/sbom.yml 43 | permissions: 44 | packages: write 45 | id-token: write 46 | with: 47 | image-name: shim-downloader 48 | image-digest: ${{ needs.build.outputs.digest }} 49 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the runtime v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=runtime.spinkube.dev 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "runtime.spinkube.dev", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /deploy/update-chart-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Note: using '-i.bak' to support different versions of sed when using in-place editing. 5 | 6 | # Swap tag in for main for URLs if the version is vx.x.x* 7 | if [[ "${APP_VERSION}" =~ ^v[0-9]+.[0-9]+.[0-9]+(.*)? ]]; then 8 | sed -i.bak -e "s%spinframework/runtime-class-manager/main%spinframework/runtime-class-manager/${APP_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/README.md" 9 | sed -i.bak -e "s%spinframework/runtime-class-manager/main%spinframework/runtime-class-manager/${APP_VERSION}%g" "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/templates/NOTES.txt" 10 | fi 11 | 12 | ## Update Chart.yaml with CHART_VERSION and APP_VERSION 13 | yq -i '.version = env(CHART_VERSION)' "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/Chart.yaml" 14 | yq -i '.appVersion = env(APP_VERSION)' "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/Chart.yaml" 15 | 16 | ## Update values.yaml tags 17 | yq -i '.image.tag = env(APP_VERSION)' "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/values.yaml" 18 | yq -i '.rcm.shimDownloaderImage.tag = env(APP_VERSION)' "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/values.yaml" 19 | yq -i '.rcm.nodeInstallerImage.tag = env(APP_VERSION)' "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}/values.yaml" 20 | 21 | # Cleanup 22 | find "${STAGING_DIR}/${CHART_NAME}-${CHART_VERSION}" -type f -name '*.bak' -print0 | xargs -0 rm -- || true -------------------------------------------------------------------------------- /deploy/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: runtime-class-manager 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | # NOTE: this version is kept static in version control but is bumped when packaging and releasing 19 | version: 0.1.0 20 | 21 | # This is the version number of the application being deployed. This version number should be 22 | # incremented each time you make changes to the application. Versions are not expected to 23 | # follow Semantic Versioning. They should reflect the version the application is using. 24 | # It is recommended to use it with quotes. 25 | # NOTE: this version is kept static in version control but is bumped when packaging and releasing 26 | appVersion: "0.1.0" 27 | -------------------------------------------------------------------------------- /config/samples/test_shim_spin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: runtime.spinkube.dev/v1alpha1 2 | kind: Shim 3 | metadata: 4 | name: spin-v2 5 | labels: 6 | app.kubernetes.io/name: spin-v2 7 | app.kubernetes.io/instance: spin-v2 8 | app.kubernetes.io/part-of: runtime-class-manager 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: runtime-class-manager 11 | spec: 12 | nodeSelector: 13 | spin: "true" 14 | 15 | fetchStrategy: 16 | type: anonymousHttp 17 | anonHttp: 18 | location: "https://github.com/spinframework/containerd-shim-spin/releases/download/v0.19.0/containerd-shim-spin-v2-linux-aarch64.tar.gz" 19 | 20 | # Each runtime can provide a set of containerd runtime options to be set in the containerd 21 | # configuration file. 22 | containerdRuntimeOptions: 23 | # The following option to pass cgroup driver information is available to runwasi based runtimes. 24 | # For runwasi, the default cgroup driver is cgroupfs. Failure to configure the correct cgroup 25 | # driver for runwasi shims may result in pod metrics failing to propagate accurately. 26 | SystemdCgroup: "true" 27 | 28 | runtimeClass: 29 | # Note: this name is used by the Spin Operator project as its default: 30 | # https://github.com/spinframework/spin-operator/blob/main/config/samples/spin-shim-executor.yaml 31 | name: wasmtime-spin-v2 32 | handler: spin-v2 33 | 34 | rolloutStrategy: 35 | type: recreate 36 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | # branches to consider in the event; optional, defaults to all 7 | branches: 8 | - main 9 | # pull_request event is required only for autolabeler 10 | pull_request: 11 | # Only following types are handled by the action, but one can default to all as well 12 | types: [opened, reopened, synchronize, edited] 13 | # pull_request_target event is required for autolabeler to support PRs from forks 14 | pull_request_target: 15 | types: [opened, reopened, synchronize, edited] 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | update_release_draft: 22 | permissions: 23 | # write permission is required to create a github release 24 | contents: write 25 | # write permission is required for autolabeler 26 | # otherwise, read permission is required at least 27 | pull-requests: write 28 | runs-on: ubuntu-latest 29 | steps: 30 | # Drafts your next Release notes as Pull Requests are merged into "master" 31 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 32 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 33 | # with: 34 | # config-name: my-config.yml 35 | # disable-autolabeler: true 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.25@sha256:a22b2e6c5e753345b9759fba9e5c1731ebe28af506745e98f406cc85d50c828e AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY . . 16 | 17 | # Build 18 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 19 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 20 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 21 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 22 | RUN --mount=type=cache,target=/root/.cache/go-build \ 23 | CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make golangci-build 24 | 25 | # Use distroless as minimal base image to package the manager binary 26 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 27 | FROM gcr.io/distroless/static:nonroot@sha256:2b7c93f6d6648c11f0e80a48558c8f77885eb0445213b8e69a6a0d7c89fc6ae4 28 | WORKDIR / 29 | COPY --from=builder /workspace/bin/manager . 30 | USER 65532:65532 31 | 32 | ENTRYPOINT ["/manager"] 33 | -------------------------------------------------------------------------------- /deploy/helm/README.md: -------------------------------------------------------------------------------- 1 | # runtime-class-manager 2 | 3 | runtime-class-manager is a Kubernetes operator that manages installation of Wasm shims onto nodes and related Runtimeclasses via [Shim custom resources](../../config/crd/bases/runtime.spinkube.dev_shims.yaml). 4 | 5 | ## Prerequisites 6 | 7 | - [Kubernetes v1.20+](https://kubernetes.io/docs/setup/) 8 | - [Helm v3](https://helm.sh/docs/intro/install/) 9 | 10 | ## Installing the chart 11 | 12 | The following installs the runtime-class-manager chart with the release name `rcm`: 13 | 14 | ```shell 15 | helm upgrade --install \ 16 | --namespace rcm \ 17 | --create-namespace \ 18 | --wait \ 19 | rcm . 20 | ``` 21 | 22 | ## Post-installation 23 | 24 | With runtime-class-manager running, you're ready to create one or more Wasm Shims. See the samples in the [config/samples directory](../../config/samples/). 25 | 26 | > Note: Ensure that the `location` for the specified shim binary points to the correct architecture for your Node(s) 27 | 28 | For example, here we install the Spin shim: 29 | 30 | ```shell 31 | kubectl apply -f https://raw.githubusercontent.com/spinframework/runtime-class-manager/refs/heads/main/config/samples/test_shim_spin.yaml 32 | ``` 33 | 34 | Now when you annotate one or more nodes with a label corresponding to the `nodeSelector` declared in the Shim, runtime-class-manager will install the shim as well as create the corresponding Runtimeclass: 35 | 36 | ```shell 37 | kubectl label node --all spin=true 38 | ``` 39 | 40 | You are now ready to deploy your Wasm workloads. 41 | -------------------------------------------------------------------------------- /internal/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "log/slog" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/spf13/afero" 12 | ) 13 | 14 | type State struct { 15 | Shims map[string]*Shim `json:"shims"` 16 | fs afero.Fs 17 | lockFilePath string 18 | } 19 | 20 | func Get(fs afero.Fs, rcmPath string) (*State, error) { 21 | out := State{ 22 | Shims: make(map[string]*Shim), 23 | lockFilePath: filepath.Join(rcmPath, "rcm-lock.json"), 24 | fs: fs, 25 | } 26 | content, err := afero.ReadFile(fs, out.lockFilePath) 27 | if err == nil { 28 | err := json.Unmarshal(content, &out) 29 | return &out, err 30 | } 31 | if !errors.Is(err, os.ErrNotExist) { 32 | return nil, err 33 | } 34 | 35 | return &out, nil 36 | } 37 | 38 | func (l *State) ShimChanged(shimName string, sha256 []byte, path string) bool { 39 | shim, ok := l.Shims[shimName] 40 | if !ok { 41 | return true 42 | } 43 | 44 | return !bytes.Equal(shim.Sha256, sha256) || shim.Path != path 45 | } 46 | 47 | func (l *State) UpdateShim(shimName string, shim Shim) { 48 | l.Shims[shimName] = &shim 49 | } 50 | 51 | func (l *State) RemoveShim(shimName string) { 52 | delete(l.Shims, shimName) 53 | } 54 | 55 | func (l *State) Write() error { 56 | out, err := json.MarshalIndent(l, "", " ") 57 | if err != nil { 58 | return err 59 | } 60 | 61 | slog.Debug("writing lock file", "content", string(out)) 62 | 63 | return afero.WriteFile(l.fs, l.lockFilePath, out, 0644) //nolint:mnd // file permissions 64 | } 65 | -------------------------------------------------------------------------------- /docs/supported_distros.md: -------------------------------------------------------------------------------- 1 | # Supported-Matrix 2 | 3 | We support the same Kubernetes distributions as Kwasm. 4 | 5 | | Container runtimes | Kind | Azure Kubernetes | GCP Kubernetes | AWS Kubernetes | Digital Ocean Kubernetes | CIVO Kubernetes | Kubernetes in Docker | Minikube | Canonical MicroK8s | 6 | |---------------------|------|------------------|----------------|----------------|--------------------------|-----------------|----------------------|----------|--------------------| 7 | | WasmEdge | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 8 | | Wasmtime | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 9 | | Fermion Spin | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 10 | | Wasm Workers Server | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 11 | | Lunatic | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 12 | | Slight | ✅ | ✅ | (✅) | (✅) | ✅ | ✅ | ✅ | ✅ | ✅ | 13 | 14 | ✅ = officially supported 15 | (✅) = only with Ubuntu Nodes -------------------------------------------------------------------------------- /images/test/Dockerfile.minikube-custom: -------------------------------------------------------------------------------- 1 | # The upstream minikube container is using expired OpenSUSE keys. 2 | # This fetches the updated keys to enable updating packages. 3 | # This is copied from the containerd-shim-spin project: https://github.com/spinframework/containerd-shim-spin/pull/289/commits/cc2e3de2a38935b50940b909707ffcaf42d3769e 4 | FROM gcr.io/k8s-minikube/kicbase:v0.0.46@sha256:fd2d445ddcc33ebc5c6b68a17e6219ea207ce63c005095ea1525296da2d1a279 5 | 6 | RUN apt-get update -y || true && \ 7 | apt-get -y install wget curl apt-transport-https ca-certificates gnupg2 && \ 8 | # Remove existing repository configurations to avoid conflicts 9 | rm -f /etc/apt/sources.list.d/devel:kubic:*.list && \ 10 | mkdir -p /etc/apt/keyrings && \ 11 | curl -fsSL "https://downloadcontent.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_22.04/Release.key" | gpg --dearmor > /etc/apt/keyrings/libcontainers-stable.gpg && \ 12 | curl -fsSL "https://downloadcontent.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.24/xUbuntu_22.04/Release.key" | gpg --dearmor > /etc/apt/keyrings/crio-stable.gpg && \ 13 | echo "deb [signed-by=/etc/apt/keyrings/libcontainers-stable.gpg] https://downloadcontent.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_22.04/ /" > /etc/apt/sources.list.d/devel-kubic-libcontainers-stable.list && \ 14 | echo "deb [signed-by=/etc/apt/keyrings/crio-stable.gpg] https://downloadcontent.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.24/xUbuntu_22.04/ /" > /etc/apt/sources.list.d/devel-kubic-libcontainers-crio-stable.list && \ 15 | apt-get update -y || true -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '⚠️ Breaking changes' 3 | labels: 4 | - 'kind/major' 5 | - 'kind/breaking-change' 6 | - title: '🚀 Features' 7 | labels: 8 | - 'kind/enhancement' 9 | - 'kind/feature' 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'kind/bug' 13 | - title: '🧰 Maintenance' 14 | labels: 15 | - 'kind/chore' 16 | - 'area/dependencies' 17 | 18 | exclude-labels: 19 | - duplicate 20 | - invalid 21 | - later 22 | - wontfix 23 | - kind/question 24 | - release/skip-changelog 25 | 26 | change-template: '- $TITLE (#$NUMBER)' 27 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 28 | name-template: 'v$RESOLVED_VERSION' 29 | template: | 30 | $CHANGES 31 | 32 | autolabeler: 33 | # Tag any PR with "!" in the subject as major update. In other words, breaking change 34 | - label: 'kind/breaking-change' 35 | title: '/.*!:.*/' 36 | - label: 'area/dependencies' 37 | title: 'chore(deps)' 38 | - label: 'area/dependencies' 39 | title: 'fix(deps)' 40 | - label: 'area/dependencies' 41 | title: 'build(deps)' 42 | - label: 'kind/feature' 43 | title: 'feat' 44 | - label: 'kind/bug' 45 | title: 'fix' 46 | - label: 'kind/chore' 47 | title: 'chore' 48 | 49 | version-resolver: 50 | major: 51 | labels: 52 | - 'kind/major' 53 | - 'kind/breaking-change' 54 | minor: 55 | labels: 56 | - 'kind/minor' 57 | - 'kind/feature' 58 | - 'kind/enhancement' 59 | patch: 60 | labels: 61 | - 'kind/patch' 62 | - 'kind/fix' 63 | - 'kind/bug' 64 | - 'kind/chore' 65 | - 'area/dependencies' 66 | default: patch 67 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 10 | name: "Dependency review" 11 | on: 12 | pull_request: 13 | branches: ["main"] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | dependency-review: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option 23 | pull-requests: write 24 | steps: 25 | - name: "Checkout repository" 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 27 | - name: "Dependency Review" 28 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 29 | # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. 30 | with: 31 | comment-summary-in-pr: always 32 | allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, BSL-1.0, CC0-1.0, ISC, MIT, MPL-2.0, Unlicense 33 | # fail-on-severity: moderate 34 | # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later 35 | # retry-on-snapshot-warnings: true 36 | -------------------------------------------------------------------------------- /internal/containerd/restart_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | // +build unix 3 | 4 | package containerd //nolint:testpackage // whitebox test 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/mitchellh/go-ps" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type mockProcess struct { 16 | executable string 17 | pid int 18 | } 19 | 20 | func (p *mockProcess) Executable() string { 21 | return p.executable 22 | } 23 | 24 | func (p *mockProcess) Pid() int { 25 | return p.pid 26 | } 27 | 28 | func (p *mockProcess) PPid() int { 29 | return 0 30 | } 31 | 32 | func Test_getPid(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | psProccessesMock func() ([]ps.Process, error) 36 | want int 37 | wantErr bool 38 | }{ 39 | {"no containerd process found", func() ([]ps.Process, error) { 40 | return []ps.Process{}, nil 41 | }, 0, true}, 42 | {"single containerd process found", func() ([]ps.Process, error) { 43 | return []ps.Process{ 44 | &mockProcess{executable: "containerd", pid: 123}, 45 | }, nil 46 | }, 123, false}, 47 | {"multiple containerd processes found", func() ([]ps.Process, error) { 48 | return []ps.Process{ 49 | &mockProcess{executable: "containerd", pid: 0}, 50 | &mockProcess{executable: "containerd", pid: 0}, 51 | }, nil 52 | }, 0, true}, 53 | {"error getting processes", func() ([]ps.Process, error) { 54 | return nil, fmt.Errorf("error getting processes") 55 | }, 0, true}, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | psProcesses = tt.psProccessesMock 60 | got, err := getPid("containerd") 61 | 62 | if tt.wantErr { 63 | require.Error(t, err) 64 | } else { 65 | require.NoError(t, err) 66 | } 67 | 68 | assert.Equal(t, tt.want, got) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Building 4 | 5 | Pre-requisites: 6 | 7 | - make 8 | - Go compiler 9 | 10 | To build the controller use the following command: 11 | 12 | ```console 13 | make all 14 | ``` 15 | 16 | To run the unit tests: 17 | 18 | ```console 19 | make test 20 | ``` 21 | 22 | ## Development 23 | 24 | To run the controller for development purposes, you can use [Tilt](https://tilt.dev/). 25 | 26 | ### Pre-requisites 27 | 28 | Please follow the [Tilt installation documentation](https://docs.tilt.dev/install.html) to install the command line tool. 29 | 30 | A development Kubernetes cluster is needed to run the controller. 31 | You can use [k3d](https://k3d.io/) to create a local cluster for development purposes. 32 | 33 | ### Settings 34 | 35 | The `tilt-settings.yaml.example` acts as a template for the `tilt-settings.yaml` file that you need to create in the root of this repository. 36 | Copy the example file and edit it to match your environment. 37 | The `tilt-settings.yaml` file is ignored by git, so you can safely edit it without worrying about committing it by mistake. 38 | 39 | The following settings can be configured: 40 | 41 | - `registry`: the container registry where the controller image will be pushed. 42 | If you don't have a private registry, you can use `ghcr.io` as long as your 43 | cluster has access to it. 44 | 45 | Example: 46 | 47 | ```yaml 48 | registry: ghcr.io/your-gh-username/runtime-class-manager 49 | ``` 50 | 51 | ### Running the controller 52 | 53 | The `Tiltfile` included in this repository will take care of the following: 54 | 55 | - Create the `runtime-class-manager` namespace and install the controller helm-chart in it. 56 | - Inject the development image in the deployment. 57 | - Automatically reload the controller when you make changes to the code. 58 | 59 | To run the controller, you just need to run the following command against an empty cluster: 60 | 61 | ```console 62 | tilt up --stream 63 | ``` 64 | -------------------------------------------------------------------------------- /deploy/helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "rcm.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "rcm.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "rcm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "rcm.labels" -}} 37 | helm.sh/chart: {{ include "rcm.chart" . }} 38 | {{ include "rcm.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "rcm.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "rcm.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "rcm.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "rcm.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /internal/containerd/install_dbus.go: -------------------------------------------------------------------------------- 1 | package containerd 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | ) 7 | 8 | // InstallDbus checks if D-Bus service is installed and active. If not, installs D-Bus 9 | // and starts the service. 10 | // NOTE: this limits support to systems using systemctl to manage systemd. 11 | func InstallDbus() error { 12 | cmd := nsenterCmd("systemctl", "start", "dbus", "--quiet") 13 | if err := cmd.Run(); err == nil { 14 | slog.Info("D-Bus is already installed and running") 15 | return nil 16 | } 17 | slog.Info("installing D-Bus") 18 | 19 | type pkgManager struct { 20 | name string 21 | check []string 22 | update []string 23 | install []string 24 | } 25 | 26 | managers := []pkgManager{ 27 | {"apt-get", []string{"which", "apt-get"}, []string{"apt-get", "update", "--yes"}, []string{"apt-get", "install", "--yes", "dbus"}}, 28 | {"dnf", []string{"which", "dnf"}, []string{}, []string{"dnf", "install", "--yes", "dbus"}}, 29 | {"apk", []string{"which", "apk"}, []string{}, []string{"apk", "add", "dbus"}}, 30 | {"yum", []string{"which", "yum"}, []string{}, []string{"yum", "install", "--yes", "dbus"}}, 31 | } 32 | installed := false 33 | for _, mgr := range managers { 34 | if err := nsenterCmd(mgr.check...).Run(); err == nil { 35 | if len(mgr.update) != 0 { 36 | if err := nsenterCmd(mgr.update...).Run(); err != nil { 37 | return fmt.Errorf("failed to update package manager %s: %w", mgr.name, err) 38 | } 39 | } 40 | if err := nsenterCmd(mgr.install...).Run(); err != nil { 41 | return fmt.Errorf("failed to install D-Bus with %s: %w", mgr.name, err) 42 | } 43 | installed = true 44 | break 45 | } 46 | } 47 | 48 | if !installed { 49 | return fmt.Errorf("could not install D-Bus as no supported package manager found") 50 | } 51 | 52 | slog.Info("restarting D-Bus") 53 | cmd = nsenterCmd("systemctl", "restart", "dbus") 54 | if err := cmd.Run(); err != nil { 55 | return fmt.Errorf("failed to restart D-Bus: %w", err) 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/shim/install.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shim 18 | 19 | import ( 20 | "crypto/sha256" 21 | "io" 22 | "os" 23 | "path" 24 | "path/filepath" 25 | 26 | "github.com/spinframework/runtime-class-manager/internal/state" 27 | ) 28 | 29 | func (c *Config) Install(shimName string) (filePath string, changed bool, err error) { 30 | shimPath := filepath.Join(c.assetPath, shimName) 31 | srcFile, err := c.rootFs.OpenFile(shimPath, os.O_RDONLY, 0o000) //nolint:mnd // file permissions 32 | if err != nil { 33 | return "", false, err 34 | } 35 | dstFilePath := path.Join(c.rcmPath, "bin", shimName) 36 | 37 | err = c.hostFs.MkdirAll(path.Dir(dstFilePath), 0o775) //nolint:mnd // file permissions 38 | if err != nil { 39 | return dstFilePath, false, err 40 | } 41 | 42 | dstFile, err := c.hostFs.OpenFile(dstFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) //nolint:mnd // file permissions 43 | if err != nil { 44 | return "", false, err 45 | } 46 | 47 | st, err := state.Get(c.hostFs, c.rcmPath) 48 | if err != nil { 49 | return "", false, err 50 | } 51 | shimSha256 := sha256.New() 52 | 53 | _, err = io.Copy(io.MultiWriter(dstFile, shimSha256), srcFile) 54 | runtimeName := RuntimeName(shimName) 55 | changed = st.ShimChanged(runtimeName, shimSha256.Sum(nil), dstFilePath) 56 | if changed { 57 | st.UpdateShim(runtimeName, state.Shim{ 58 | Path: dstFilePath, 59 | Sha256: shimSha256.Sum(nil), 60 | }) 61 | if err := st.Write(); err != nil { 62 | return "", false, err 63 | } 64 | } 65 | 66 | return dstFilePath, changed, err 67 | } 68 | -------------------------------------------------------------------------------- /example/node-installer/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: kwasm-initializer 5 | labels: 6 | app: default-init 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: default-init 11 | updateStrategy: 12 | type: RollingUpdate 13 | template: 14 | metadata: 15 | labels: 16 | name: kwasm-initializer 17 | app: default-init 18 | spec: 19 | hostPID: true 20 | volumes: 21 | - name: node-root 22 | hostPath: 23 | path: / 24 | - name: assets 25 | emptyDir: {} 26 | initContainers: 27 | - image: busybox:latest 28 | name: shim-downloader 29 | env: 30 | volumeMounts: 31 | - name: assets 32 | mountPath: /assets 33 | command: ["/bin/sh", "-c"] 34 | args: 35 | - | 36 | wget -O- https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.9.1/containerd-wasm-shims-v1-lunatic-linux-$(uname -m).tar.gz | tar xzf - -C /assets; 37 | wget -O- https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.9.1/containerd-wasm-shims-v1-slight-linux-$(uname -m).tar.gz | tar -xzf - -C /assets; 38 | wget -O- https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.9.1/containerd-wasm-shims-v1-spin-linux-$(uname -m).tar.gz | tar -xzf - -C /assets; 39 | wget -O- https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.9.1/containerd-wasm-shims-v1-wws-linux-$(uname -m).tar.gz | tar -xzf - -C /assets 40 | - image: ghcr.io/kwasm/kwasm-node-installer:pr-46 41 | name: kwasm-initializer 42 | imagePullPolicy: Always 43 | args: 44 | - "install" 45 | - "-H" 46 | - "/mnt/node-root" 47 | securityContext: 48 | privileged: true 49 | volumeMounts: 50 | - name: assets 51 | mountPath: /assets 52 | - name: node-root 53 | mountPath: /mnt/node-root/ 54 | containers: 55 | - image: k8s.gcr.io/pause:3.1 56 | name: pause 57 | -------------------------------------------------------------------------------- /internal/preset/preset_test.go: -------------------------------------------------------------------------------- 1 | package preset_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/afero" 7 | "github.com/spinframework/runtime-class-manager/internal/preset" 8 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_WithSetup(t *testing.T) { 13 | type args struct { 14 | settings preset.Settings 15 | hostFs afero.Fs 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | wantErr bool 21 | wantContents string 22 | }{ 23 | { 24 | "rke2_err", 25 | args{ 26 | preset.RKE2, 27 | tests.FixtureFs("../../testdata/node-installer/distros/unsupported"), 28 | }, 29 | true, 30 | "", 31 | }, 32 | { 33 | "rke2_config_exists", 34 | args{ 35 | preset.RKE2, 36 | tests.FixtureFs("../../testdata/node-installer/containerd/rke2-existing-config-tmpl"), 37 | }, 38 | false, 39 | "version = 2\npreexisting-config = true", 40 | }, 41 | { 42 | "rke2_config_is_created", 43 | args{ 44 | preset.RKE2, 45 | tests.FixtureFs("../../testdata/node-installer/distros/rke2"), 46 | }, 47 | false, 48 | "version = 2", 49 | }, 50 | { 51 | "k3s", 52 | args{ 53 | preset.K3s, 54 | tests.FixtureFs("../../testdata/node-installer/distros/k3s"), 55 | }, 56 | false, 57 | "version = 2", 58 | }, 59 | { 60 | "k0s", 61 | args{ 62 | preset.K0s, 63 | tests.FixtureFs("../../testdata/node-installer/distros/k0s"), 64 | }, 65 | false, 66 | "", 67 | }, 68 | { 69 | "microk8s", 70 | args{ 71 | preset.MicroK8s, 72 | tests.FixtureFs("../../testdata/node-installer/distros/microk8s"), 73 | }, 74 | false, 75 | "", 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | err := tt.args.settings.Setup( 81 | preset.Env{ 82 | ConfigPath: tt.args.settings.ConfigPath, 83 | HostFs: tt.args.hostFs, 84 | }, 85 | ) 86 | 87 | if tt.wantErr { 88 | require.Error(t, err) 89 | } else { 90 | require.NoError(t, err) 91 | bytes, err := afero.ReadFile(tt.args.hostFs, tt.args.settings.ConfigPath) 92 | require.NoError(t, err) 93 | require.Equal(t, tt.wantContents, string(bytes)) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/shim/uninstall_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shim //nolint:testpackage // whitebox test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/spf13/afero" 23 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 24 | ) 25 | 26 | func TestConfig_Uninstall(t *testing.T) { 27 | type fields struct { 28 | hostFs afero.Fs 29 | rcmPath string 30 | } 31 | type args struct { 32 | shimName string 33 | } 34 | tests := []struct { 35 | name string 36 | fields fields 37 | args args 38 | want string 39 | wantErr bool 40 | }{ 41 | { 42 | "shim not installed", 43 | fields{ 44 | tests.FixtureFs("../../testdata/node-installer/shim"), 45 | "/opt/rcm", 46 | }, 47 | args{"not-existing-shim"}, 48 | "", 49 | true, 50 | }, 51 | { 52 | "missing shim binary", 53 | fields{ 54 | tests.FixtureFs("../../testdata/node-installer/shim-missing-binary"), 55 | "/opt/rcm", 56 | }, 57 | args{"spin-v1"}, 58 | "/opt/rcm/bin/containerd-shim-spin-v1", 59 | false, 60 | }, 61 | { 62 | "successful shim uninstallation", 63 | fields{ 64 | tests.FixtureFs("../../testdata/node-installer/shim"), 65 | "/opt/rcm", 66 | }, 67 | args{"spin-v1"}, 68 | "/opt/rcm/bin/containerd-shim-spin-v1", 69 | false, 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | c := &Config{ 75 | hostFs: tt.fields.hostFs, 76 | rcmPath: tt.fields.rcmPath, 77 | } 78 | 79 | got, err := c.Uninstall(tt.args.shimName) 80 | 81 | if (err != nil) != tt.wantErr { 82 | t.Errorf("Config.Uninstall() error = %v, wantErr %v", err, tt.wantErr) 83 | return 84 | } 85 | if got != tt.want { 86 | t.Errorf("Config.Uninstall() = %v, want %v", got, tt.want) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/node-installer/detect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SpinKube Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "log/slog" 23 | 24 | "github.com/spf13/afero" 25 | "github.com/spinframework/runtime-class-manager/internal/preset" 26 | ) 27 | 28 | const defaultContainerdConfigLocation = "/etc/containerd/config.toml" 29 | 30 | var containerdConfigLocations = map[string]preset.Settings{ 31 | // Microk8s 32 | "/var/snap/microk8s/current/args/containerd-template.toml": preset.MicroK8s, 33 | // RKE2 34 | "/var/lib/rancher/rke2/agent/etc/containerd/config.toml": preset.RKE2, 35 | // K3s 36 | "/var/lib/rancher/k3s/agent/etc/containerd/config.toml": preset.K3s, 37 | // K0s 38 | "/etc/k0s/containerd.toml": preset.K0s, 39 | } 40 | 41 | func DetectDistro(config Config, hostFs afero.Fs) (preset.Settings, error) { 42 | if config.Runtime.ConfigPath != "" { 43 | // containerd config path has been set explicitly 44 | if distro, ok := containerdConfigLocations[config.Runtime.ConfigPath]; ok { 45 | return distro, nil 46 | } 47 | slog.Warn("could not determine distro from containerd config, falling back to defaults", "config", config.Runtime.ConfigPath) 48 | return preset.Default.WithConfigPath(config.Runtime.ConfigPath), nil 49 | } 50 | 51 | var errs []error 52 | 53 | // Check for distro-specific containerd config locations first. 54 | // We do this because the default config may *also* exist in some scenarios. 55 | for loc, distro := range containerdConfigLocations { 56 | _, err := hostFs.Stat(loc) 57 | if err == nil { 58 | // config file found, return corresponding distro settings 59 | return distro, nil 60 | } 61 | errs = append(errs, err) 62 | } 63 | 64 | // Check the default location last, assuming no distro-specific location has been detected. 65 | _, err := hostFs.Stat(defaultContainerdConfigLocation) 66 | if err == nil { 67 | return preset.Default, nil 68 | } 69 | errs = append(errs, err) 70 | 71 | return preset.Settings{}, fmt.Errorf("failed to detect containerd config path: %w", errors.Join(errs...)) 72 | } 73 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # -*- mode: Python -*- 2 | 3 | tilt_settings_file = "./tilt-settings.yaml" 4 | settings = read_yaml(tilt_settings_file) 5 | 6 | kubectl_cmd = "kubectl" 7 | 8 | # verify kubectl command exists 9 | if str(local("command -v " + kubectl_cmd + " || true", quiet = True)) == "": 10 | fail("Required command '" + kubectl_cmd + "' not found in PATH") 11 | 12 | # Create the rcm namespace 13 | # This is required since the helm() function doesn't support the create_namespace flag 14 | load('ext://namespace', 'namespace_create') 15 | namespace_create('rcm') 16 | 17 | # Install runtime-class-manager helm chart 18 | install = helm( 19 | './deploy/helm', 20 | name='runtime-class-manager', 21 | namespace='rcm', 22 | set=['image.repository=' + settings.get('registry')] 23 | ) 24 | 25 | objects = decode_yaml_stream(install) 26 | for o in objects: 27 | # Update the root security group. Tilt requires root access to update the 28 | # running process. 29 | if o.get('kind') == 'Deployment' and o.get('metadata').get('name') == 'runtime-class-manager': 30 | o['spec']['template']['spec']['securityContext']['runAsNonRoot'] = False 31 | # Disable the leader election to speed up the startup time. 32 | o['spec']['template']['spec']['containers'][0]['args'].remove('--leader-elect') 33 | break 34 | updated_install = encode_yaml_stream(objects) 35 | k8s_yaml(updated_install) 36 | 37 | # enable hot reloading by doing the following: 38 | # - locally build the whole project 39 | # - create a docker imagine using tilt's hot-swap wrapper 40 | # - push that container to the local tilt registry 41 | # Once done, rebuilding now should be a lot faster since only the relevant 42 | # binary is rebuilt and the hot swat wrapper takes care of the rest. 43 | local_resource( 44 | 'manager', 45 | "CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/manager ./", 46 | deps = [ 47 | "main.go", 48 | "go.mod", 49 | "go.sum", 50 | "controllers", 51 | ], 52 | ) 53 | 54 | # Build the docker image for our controller. We use a specific Dockerfile 55 | # since tilt can't run on a scratch container. 56 | entrypoint = ['/manager', '-zap-devel'] 57 | dockerfile = 'tilt.dockerfile' 58 | 59 | load('ext://restart_process', 'docker_build_with_restart') 60 | docker_build_with_restart( 61 | settings.get('registry'), 62 | '.', 63 | dockerfile = dockerfile, 64 | entrypoint = entrypoint, 65 | # `only` here is important, otherwise, the container will get updated 66 | # on _any_ file change. 67 | only=[ 68 | './bin', 69 | ], 70 | live_update = [ 71 | sync('./bin/manager', '/manager'), 72 | ], 73 | ) 74 | 75 | -------------------------------------------------------------------------------- /internal/preset/preset.go: -------------------------------------------------------------------------------- 1 | package preset 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/afero" 10 | "github.com/spinframework/runtime-class-manager/internal/containerd" 11 | ) 12 | 13 | type Settings struct { 14 | ConfigPath string 15 | Setup func(Env) error 16 | Restarter containerd.Restarter 17 | } 18 | 19 | type Env struct { 20 | HostFs afero.Fs 21 | ConfigPath string 22 | } 23 | 24 | var Default = Settings{ 25 | ConfigPath: "/etc/containerd/config.toml", 26 | Setup: func(_ Env) error { return nil }, 27 | Restarter: containerd.NewDefaultRestarter(), 28 | } 29 | 30 | func (s Settings) WithConfigPath(path string) Settings { 31 | s.ConfigPath = path 32 | return s 33 | } 34 | 35 | func (s Settings) WithSetup(setup func(env Env) error) Settings { 36 | s.Setup = setup 37 | return s 38 | } 39 | 40 | func (s Settings) WithRestarter(restarter containerd.Restarter) Settings { 41 | s.Restarter = restarter 42 | return s 43 | } 44 | 45 | var MicroK8s = Default.WithConfigPath("/var/snap/microk8s/current/args/containerd-template.toml"). 46 | WithRestarter(containerd.MicroK8sRestarter{}) 47 | 48 | var RKE2 = Default.WithConfigPath("/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl"). 49 | WithRestarter(containerd.RKE2Restarter{}). 50 | WithSetup(func(env Env) error { 51 | _, err := env.HostFs.Stat(env.ConfigPath) 52 | if err == nil { 53 | return nil 54 | } 55 | 56 | if errors.Is(err, os.ErrNotExist) { 57 | // Copy base config into .tmpl version 58 | src, _ := strings.CutSuffix(env.ConfigPath, ".tmpl") 59 | in, err := env.HostFs.Open(src) 60 | if err != nil { 61 | return err 62 | } 63 | defer in.Close() 64 | out, err := env.HostFs.Create(env.ConfigPath) 65 | if err != nil { 66 | return err 67 | } 68 | defer func() { 69 | cerr := out.Close() 70 | if err == nil { 71 | err = cerr 72 | } 73 | }() 74 | if _, err = io.Copy(out, in); err != nil { 75 | return err 76 | } 77 | err = out.Sync() 78 | 79 | return nil 80 | } 81 | 82 | return err 83 | }) 84 | 85 | var K3s = RKE2.WithConfigPath("/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl"). 86 | WithRestarter(containerd.K3sRestarter{}) 87 | 88 | var K0s = Default.WithConfigPath("/etc/k0s/containerd.d/config.toml"). 89 | WithRestarter(containerd.K0sRestarter{}). 90 | WithSetup(func(env Env) error { 91 | _, err := env.HostFs.Stat(env.ConfigPath) 92 | if err == nil { 93 | return nil 94 | } 95 | 96 | if errors.Is(err, os.ErrNotExist) { 97 | _, err := env.HostFs.Create(env.ConfigPath) 98 | if err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | return err 105 | }) 106 | -------------------------------------------------------------------------------- /deploy/helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for rcm. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: "ghcr.io/spinframework/runtime-class-manager" 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "latest" 12 | 13 | rcm: 14 | shimDownloaderImage: 15 | repository: "ghcr.io/spinframework/shim-downloader" 16 | tag: "latest" 17 | nodeInstallerImage: 18 | repository: "ghcr.io/spinframework/node-installer" 19 | tag: "latest" 20 | nodeInstallerJob: 21 | ttl: 0 22 | 23 | imagePullSecrets: [] 24 | nameOverride: "" 25 | fullnameOverride: "" 26 | 27 | serviceAccount: 28 | # Specifies whether a service account should be created 29 | create: true 30 | # Automatically mount a ServiceAccount's API credentials? 31 | automount: true 32 | # Annotations to add to the service account 33 | annotations: {} 34 | # The name of the service account to use. 35 | # If not set and create is true, a name is generated using the fullname template 36 | name: "" 37 | 38 | podAnnotations: {} 39 | podLabels: {} 40 | 41 | podSecurityContext: {} 42 | # fsGroup: 2000 43 | 44 | securityContext: {} 45 | # capabilities: 46 | # drop: 47 | # - ALL 48 | # readOnlyRootFilesystem: true 49 | # runAsNonRoot: true 50 | # runAsUser: 1000 51 | 52 | service: 53 | type: ClusterIP 54 | port: 80 55 | 56 | resources: {} 57 | # We usually recommend not to specify default resources and to leave this as a conscious 58 | # choice for the user. This also increases chances charts run on environments with little 59 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 60 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 61 | # limits: 62 | # cpu: 100m 63 | # memory: 128Mi 64 | # requests: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | 68 | livenessProbe: 69 | httpGet: 70 | path: /healthz 71 | port: 8082 72 | initialDelaySeconds: 15 73 | periodSeconds: 20 74 | readinessProbe: 75 | httpGet: 76 | path: /readyz 77 | port: 8082 78 | 79 | autoscaling: 80 | enabled: false 81 | minReplicas: 1 82 | maxReplicas: 100 83 | targetCPUUtilizationPercentage: 80 84 | # targetMemoryUtilizationPercentage: 80 85 | 86 | # Additional volumes on the output Deployment definition. 87 | volumes: [] 88 | # - name: foo 89 | # secret: 90 | # secretName: mysecret 91 | # optional: false 92 | 93 | # Additional volumeMounts on the output Deployment definition. 94 | volumeMounts: [] 95 | # - name: foo 96 | # mountPath: "/etc/foo" 97 | # readOnly: true 98 | 99 | nodeSelector: {} 100 | 101 | tolerations: [] 102 | 103 | affinity: {} 104 | -------------------------------------------------------------------------------- /internal/state/shim_test.go: -------------------------------------------------------------------------------- 1 | package state //nolint:testpackage // whitebox test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestShim_MarshalJSON(t *testing.T) { 11 | type fields struct { 12 | Sha256 []byte 13 | Path string 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | want string 19 | wantErr bool 20 | }{ 21 | { 22 | "default", 23 | fields{ 24 | Sha256: []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 25 | Path: "/opt/rcm/bin/containerd-shim-spin-v1", 26 | }, 27 | `{"sha256":"6da5e8f17a9bfa9cb04cf22c87b6475394ecec3af4fdc337f72d6dbf3319ea52","path":"/opt/rcm/bin/containerd-shim-spin-v1"}`, 28 | false, 29 | }, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | s := &Shim{ 34 | Sha256: tt.fields.Sha256, 35 | Path: tt.fields.Path, 36 | } 37 | got, err := s.MarshalJSON() 38 | if tt.wantErr { 39 | require.Error(t, err) 40 | } else { 41 | require.NoError(t, err) 42 | } 43 | assert.Equal(t, tt.want, string(got)) 44 | }) 45 | } 46 | } 47 | 48 | func TestShim_UnmarshalJSON(t *testing.T) { 49 | type wants struct { 50 | path string 51 | sha256 []byte 52 | } 53 | type args struct { 54 | data string 55 | } 56 | tests := []struct { 57 | name string 58 | args args 59 | want wants 60 | wantErr bool 61 | }{ 62 | { 63 | "default", 64 | args{ 65 | `{"sha256":"6da5e8f17a9bfa9cb04cf22c87b6475394ecec3af4fdc337f72d6dbf3319ea52","path":"/opt/rcm/bin/containerd-shim-spin-v1"}`, 66 | }, 67 | wants{ 68 | path: "/opt/rcm/bin/containerd-shim-spin-v1", 69 | sha256: []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 70 | }, 71 | false, 72 | }, 73 | { 74 | "broken sha", 75 | args{ 76 | `{"sha256":"2","path":"/opt/rcm/bin/containerd-shim-spin-v1"}`, 77 | }, 78 | wants{ 79 | path: "/opt/rcm/bin/containerd-shim-spin-v1", 80 | sha256: nil, 81 | }, 82 | true, 83 | }, 84 | { 85 | "broken json", 86 | args{ 87 | `broken`, 88 | }, 89 | wants{ 90 | path: "", 91 | sha256: nil, 92 | }, 93 | true, 94 | }, 95 | } 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | var s Shim 99 | err := s.UnmarshalJSON([]byte(tt.args.data)) 100 | 101 | if tt.wantErr { 102 | require.Error(t, err) 103 | } else { 104 | require.NoError(t, err) 105 | } 106 | assert.Equal(t, tt.want.path, s.Path) 107 | assert.Equal(t, tt.want.sha256, s.Sha256) 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /deploy/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "rcm.fullname" . }} 5 | labels: 6 | {{- include "rcm.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "rcm.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "rcm.labels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "rcm.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | env: 40 | - name: CONTROLLER_NAMESPACE 41 | value: {{ .Release.Namespace }} 42 | - name: SHIM_DOWNLOADER_IMAGE 43 | value: "{{ .Values.rcm.shimDownloaderImage.repository }}:{{ .Values.rcm.shimDownloaderImage.tag | default .Chart.AppVersion }}" 44 | - name: SHIM_NODE_INSTALLER_IMAGE 45 | value: "{{ .Values.rcm.nodeInstallerImage.repository }}:{{ .Values.rcm.nodeInstallerImage.tag | default .Chart.AppVersion }}" 46 | - name: SHIM_NODE_INSTALLER_JOB_TTL 47 | value: "{{ .Values.rcm.nodeInstallerJob.ttl | default 0 }}" 48 | ports: 49 | - name: http 50 | containerPort: {{ .Values.service.port }} 51 | protocol: TCP 52 | livenessProbe: 53 | {{- toYaml .Values.livenessProbe | nindent 12 }} 54 | readinessProbe: 55 | {{- toYaml .Values.readinessProbe | nindent 12 }} 56 | resources: 57 | {{- toYaml .Values.resources | nindent 12 }} 58 | {{- with .Values.volumeMounts }} 59 | volumeMounts: 60 | {{- toYaml . | nindent 12 }} 61 | {{- end }} 62 | {{- with .Values.volumes }} 63 | volumes: 64 | {{- toYaml . | nindent 8 }} 65 | {{- end }} 66 | {{- with .Values.nodeSelector }} 67 | nodeSelector: 68 | {{- toYaml . | nindent 8 }} 69 | {{- end }} 70 | {{- with .Values.affinity }} 71 | affinity: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | {{- with .Values.tolerations }} 75 | tolerations: 76 | {{- toYaml . | nindent 8 }} 77 | {{- end }} 78 | -------------------------------------------------------------------------------- /.github/workflows/helm-chart-release.yml: -------------------------------------------------------------------------------- 1 | # This action releases the runtime-class-manager helm chart 2 | # 3 | # A chart is published to the configured OCI registry on every push to main 4 | # as well as on semver tag releases (via workflow_call from release.yml). 5 | name: Release helm chart 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | workflow_call: 12 | 13 | permissions: 14 | contents: read 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | CHART_NAME: runtime-class-manager 19 | 20 | jobs: 21 | release: 22 | name: Release chart 23 | runs-on: ubuntu-22.04 24 | permissions: 25 | packages: write 26 | 27 | steps: 28 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 29 | 30 | - name: Install helm 31 | uses: Azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 32 | with: 33 | version: v3.16.3 34 | 35 | - name: Determine chart version 36 | run: | 37 | if [[ "${{ startsWith(github.ref, 'refs/tags/v') }}" == "true" ]]; then 38 | # NOTE: We remove the leading 'v' to comply with helm's versioning requirements 39 | echo "CHART_VERSION=$(echo -n ${{ github.ref_name }} | sed -rn 's/(v)?(.*)/\2/p')" >> $GITHUB_ENV 40 | echo "APP_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV 41 | else 42 | # NOTE: We can replace 0.0.0 with e.g. $(git describe --tags $(git rev-list --tags --max-count=1)) once we have a first tag 43 | # However, we'll also need to update the checkout step with 'fetch-depth: 0' if we list tags 44 | echo "CHART_VERSION=0.0.0-$(date +%Y%m%d-%H%M%S)-g$(git rev-parse --short HEAD)" >> $GITHUB_ENV 45 | # Setting to 'latest' to match tag used in container-image.yml 46 | echo "APP_VERSION=latest" >> $GITHUB_ENV 47 | fi 48 | 49 | - name: Log into registry ${{ env.REGISTRY }} 50 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 51 | with: 52 | registry: ${{ env.REGISTRY }} 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Package chart 57 | run: make helm-package 58 | 59 | - name: Lint packaged chart 60 | run: make helm-lint 61 | 62 | - name: Upload chart as GitHub artifact 63 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 64 | with: 65 | name: ${{ env.CHART_NAME }} 66 | path: _dist/${{ env.CHART_NAME }}-${{ env.CHART_VERSION }}.tgz 67 | 68 | - name: Publish chart 69 | env: 70 | CHART_REGISTRY: ${{ env.REGISTRY }}/${{ github.repository_owner }}/charts 71 | run: | 72 | make helm-publish 73 | 74 | echo '### Helm chart published:' >> $GITHUB_STEP_SUMMARY 75 | echo '- `Reference: ${{ env.CHART_REGISTRY }}/${{ env.CHART_NAME }}`' >> $GITHUB_STEP_SUMMARY 76 | echo '- `Version: ${{ env.CHART_VERSION }}`' >> $GITHUB_STEP_SUMMARY 77 | -------------------------------------------------------------------------------- /cmd/node-installer/uninstall.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "log/slog" 22 | "os" 23 | "path" 24 | 25 | "github.com/spf13/afero" 26 | "github.com/spf13/cobra" 27 | 28 | "github.com/spinframework/runtime-class-manager/internal/containerd" 29 | "github.com/spinframework/runtime-class-manager/internal/shim" 30 | ) 31 | 32 | // uninstallCmd represents the uninstall command. 33 | var uninstallCmd = &cobra.Command{ 34 | Use: "uninstall", 35 | Short: "Uninstall containerd shims", 36 | Run: func(_ *cobra.Command, _ []string) { 37 | rootFs := afero.NewOsFs() 38 | hostFs := afero.NewBasePathFs(rootFs, config.Host.RootPath) 39 | 40 | distro, err := DetectDistro(config, hostFs) 41 | if err != nil { 42 | slog.Error("failed to detect containerd config", "error", err) 43 | os.Exit(1) 44 | } 45 | 46 | config.Runtime.ConfigPath = distro.ConfigPath 47 | 48 | config.Runtime.Options, err = RuntimeOptions() 49 | if err != nil { 50 | slog.Error("failed to get runtime options", "error", err) 51 | os.Exit(1) 52 | } 53 | 54 | if err := RunUninstall(config, rootFs, hostFs, distro.Restarter); err != nil { 55 | slog.Error("failed to uninstall", "error", err) 56 | os.Exit(1) 57 | } 58 | }, 59 | } 60 | 61 | func init() { 62 | rootCmd.AddCommand(uninstallCmd) 63 | } 64 | 65 | func RunUninstall(config Config, rootFs, hostFs afero.Fs, restarter containerd.Restarter) error { 66 | slog.Info("uninstall called", "shim", config.Runtime.Name) 67 | shimName := config.Runtime.Name 68 | runtimeName := path.Join(config.RCM.Path, "bin", shimName) 69 | 70 | containerdConfig := containerd.NewConfig(hostFs, config.Runtime.ConfigPath, restarter, config.Runtime.Options) 71 | shimConfig := shim.NewConfig(rootFs, hostFs, config.RCM.AssetPath, config.RCM.Path) 72 | 73 | binPath, err := shimConfig.Uninstall(shimName) 74 | if err != nil { 75 | return fmt.Errorf("failed to delete shim '%s': %w", runtimeName, err) 76 | } 77 | 78 | configChanged, err := containerdConfig.RemoveRuntime(binPath) 79 | if err != nil { 80 | return fmt.Errorf("failed to write containerd config for shim '%s': %w", runtimeName, err) 81 | } 82 | 83 | if !configChanged { 84 | slog.Info("nothing changed, nothing more to do") 85 | return nil 86 | } 87 | 88 | slog.Info("restarting containerd") 89 | err = containerdConfig.RestartRuntime() 90 | if err != nil { 91 | return fmt.Errorf("failed to restart containerd: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /cmd/node-installer/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/pflag" 27 | "github.com/spf13/viper" 28 | ) 29 | 30 | var config Config 31 | 32 | // rootCmd represents the base command when called without any subcommands. 33 | var rootCmd = &cobra.Command{ 34 | Use: "rcm-node-installer", 35 | Short: "rcm-node-installer manages containerd shims", 36 | PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { 37 | return initializeConfig(cmd) 38 | }, 39 | } 40 | 41 | // Execute adds all child commands to the root command and sets flags appropriately. 42 | // This is called by main.main(). It only needs to happen once to the rootCmd. 43 | func Execute() { 44 | err := rootCmd.Execute() 45 | if err != nil { 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func init() { 51 | rootCmd.PersistentFlags().StringVarP(&config.Runtime.Name, "runtime", "r", "containerd", "Set the container runtime to configure (containerd, cri-o)") 52 | rootCmd.PersistentFlags().StringVarP(&config.Runtime.ConfigPath, "runtime-config", "c", "", "Path to the runtime config file. Will try to autodetect if left empty") 53 | rootCmd.PersistentFlags().StringVarP(&config.RCM.Path, "rcm-path", "k", "/opt/rcm", "Working directory for the RuntimeClassManager on the host") 54 | rootCmd.PersistentFlags().StringVarP(&config.Host.RootPath, "host-root", "H", "/", "Path to the host root path") 55 | } 56 | 57 | func initializeConfig(cmd *cobra.Command) error { 58 | v := viper.New() 59 | 60 | v.SetConfigName("rcm") 61 | 62 | v.AddConfigPath(".") 63 | v.AddConfigPath("/etc") 64 | 65 | if err := v.ReadInConfig(); err != nil { 66 | if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 67 | return err 68 | } 69 | } 70 | 71 | // Environment variables are prefixed with RCM_ 72 | v.SetEnvPrefix("RCM") 73 | 74 | // - is replaced with _ in environment variables 75 | v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 76 | 77 | // Bind env variables 78 | v.AutomaticEnv() 79 | 80 | bindFlags(cmd, v) 81 | 82 | return nil 83 | } 84 | 85 | // bindFlags binds each cobra flag to its associated viper configuration. 86 | func bindFlags(cmd *cobra.Command, v *viper.Viper) { 87 | cmd.Flags().VisitAll(func(f *pflag.Flag) { 88 | configName := strings.ReplaceAll(f.Name, "-", "_") 89 | 90 | if !f.Changed && v.IsSet(configName) { 91 | val := v.Get(configName) 92 | _ = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller_test 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "runtime" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | logf "sigs.k8s.io/controller-runtime/pkg/log" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | runtimev1alpha1 "github.com/spinframework/runtime-class-manager/api/v1alpha1" 36 | //+kubebuilder:scaffold:imports 37 | ) 38 | 39 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 40 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 41 | 42 | var ( 43 | cfg *rest.Config 44 | k8sClient client.Client 45 | testEnv *envtest.Environment 46 | ) 47 | 48 | func TestControllers(t *testing.T) { 49 | RegisterFailHandler(Fail) 50 | 51 | RunSpecs(t, "Controller Suite") 52 | } 53 | 54 | var _ = BeforeSuite(func() { 55 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 56 | 57 | By("bootstrapping test environment") 58 | testEnv = &envtest.Environment{ 59 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 60 | ErrorIfCRDPathMissing: true, 61 | 62 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 63 | // without call the makefile target test. If not informed it will look for the 64 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 65 | // Note that you must have the required binaries setup under the bin directory to perform 66 | // the tests directly. When we run make test it will be setup and used automatically. 67 | BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", 68 | fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), 69 | } 70 | 71 | var err error 72 | // cfg is defined in this file globally. 73 | cfg, err = testEnv.Start() 74 | Expect(err).NotTo(HaveOccurred()) 75 | Expect(cfg).NotTo(BeNil()) 76 | 77 | err = runtimev1alpha1.AddToScheme(scheme.Scheme) 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | //+kubebuilder:scaffold:scheme 81 | 82 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 83 | Expect(err).NotTo(HaveOccurred()) 84 | Expect(k8sClient).NotTo(BeNil()) 85 | }) 86 | 87 | var _ = AfterSuite(func() { 88 | By("tearing down the test environment") 89 | err := testEnv.Stop() 90 | Expect(err).NotTo(HaveOccurred()) 91 | }) 92 | -------------------------------------------------------------------------------- /.github/workflows/sbom.yml: -------------------------------------------------------------------------------- 1 | name: Generate SBOMs 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | image-name: 9 | type: string 10 | required: true 11 | image-digest: 12 | type: string 13 | required: true 14 | 15 | jobs: 16 | sbom: 17 | name: Generate SBOM, sign and attach them to OCI image 18 | strategy: 19 | matrix: 20 | arch: [amd64, arm64] 21 | 22 | permissions: 23 | packages: write 24 | id-token: write 25 | 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Install cosign 29 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 30 | 31 | - name: Install the syft command 32 | uses: anchore/sbom-action/download-syft@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.11 33 | 34 | - name: Install the crane command 35 | uses: IAreKyleW00t/crane-installer@3bed13b272649677427c31d0a0dd1566bbb38f7f # v4.0.5 36 | 37 | - name: Login to GitHub Container Registry 38 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.repository_owner }} 42 | password: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Find platform digest 45 | shell: bash 46 | run: | 47 | set -e 48 | DIGEST=$(crane digest \ 49 | --platform "linux/${{ matrix.arch }}" \ 50 | ghcr.io/${{ github.repository_owner}}/${{ inputs.image-name }}@${{ inputs.image-digest }}) 51 | echo "PLATFORM_DIGEST=${DIGEST}" >> "$GITHUB_ENV" 52 | 53 | - name: Create SBOM file 54 | shell: bash 55 | run: | 56 | syft \ 57 | -o spdx-json \ 58 | --file ${{ inputs.image-name }}-sbom-${{ matrix.arch }}.spdx \ 59 | ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}@${{ env.PLATFORM_DIGEST }} 60 | 61 | - name: Sign SBOM file 62 | run: | 63 | cosign sign-blob --yes \ 64 | --bundle ${{ inputs.image-name }}-sbom-${{ matrix.arch }}.spdx.bundle \ 65 | ${{ inputs.image-name }}-sbom-${{ matrix.arch }}.spdx 66 | 67 | - name: Attach SBOM file in the container image 68 | shell: bash 69 | run: | 70 | cosign attach \ 71 | sbom --sbom ${{ inputs.image-name }}-sbom-${{ matrix.arch }}.spdx \ 72 | ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}@${{ env.PLATFORM_DIGEST }} 73 | 74 | - name: Sign SBOM file pushed to OCI registry 75 | shell: bash 76 | run: | 77 | set -e 78 | SBOM_TAG="$(echo ${{ env.PLATFORM_DIGEST }} | sed -e 's/:/-/g').sbom" 79 | 80 | cosign sign --yes \ 81 | ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}:${SBOM_TAG} 82 | 83 | - name: Upload SBOMs as artifacts 84 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 85 | with: 86 | name: ${{ inputs.image-name }}-sbom-${{ matrix.arch }} 87 | path: ${{ inputs.image-name }}-sbom-* 88 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 3 * * 0' 14 | push: 15 | branches: [ "main" ] 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.github/workflows/container-image.yml: -------------------------------------------------------------------------------- 1 | name: Build container image 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | inputs: 9 | image-name: 10 | type: string 11 | required: true 12 | dockerfile: 13 | type: string 14 | required: true 15 | docker-context: 16 | type: string 17 | required: false 18 | default: "." 19 | push-image: 20 | type: boolean 21 | required: true 22 | outputs: 23 | repository: 24 | description: "Repository used to build the container image" 25 | value: ${{ jobs.build.outputs.repository }} 26 | tag: 27 | description: "Tag used to build the container image" 28 | value: ${{ jobs.build.outputs.tag }} 29 | digest: 30 | description: "Image digest" 31 | value: ${{ jobs.build.outputs.digest }} 32 | 33 | jobs: 34 | build: 35 | name: Build container image 36 | permissions: 37 | contents: read 38 | packages: write 39 | runs-on: ubuntu-latest 40 | outputs: 41 | repository: ${{ steps.setoutput.outputs.repository }} 42 | tag: ${{ steps.setoutput.outputs.tag }} 43 | artifact: ${{ steps.setoutput.outputs.artifact }} 44 | digest: ${{ steps.setoutput.outputs.digest }} 45 | steps: 46 | - name: Checkout code 47 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 48 | - name: Set up QEMU 49 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 50 | - name: Set up Docker Buildx 51 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 52 | - name: Login to GitHub Container Registry 53 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.repository_owner }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | - name: Retrieve tag name (main branch) 59 | if: ${{ startsWith(github.ref, 'refs/heads/main') }} 60 | run: | 61 | echo TAG_NAME=latest >> $GITHUB_ENV 62 | - name: Retrieve tag name (feat branch) 63 | if: ${{ startsWith(github.ref, 'refs/heads/feat') }} 64 | run: | 65 | echo "TAG_NAME=latest-$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV 66 | - name: Retrieve tag name (tag) 67 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 68 | run: | 69 | echo TAG_NAME=$(echo $GITHUB_REF | sed -e "s|refs/tags/||") >> $GITHUB_ENV 70 | - name: Build and push container image 71 | if: ${{ inputs.push-image }} 72 | id: build-image 73 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 74 | with: 75 | context: ${{ inputs.docker-context }} 76 | file: ${{ inputs.dockerfile }} 77 | platforms: linux/amd64, linux/arm64 78 | push: true 79 | tags: | 80 | ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}:${{ env.TAG_NAME }} 81 | - id: setoutput 82 | name: Set output parameters 83 | run: | 84 | echo "repository=ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}" >> $GITHUB_OUTPUT 85 | echo "tag=${{ env.TAG_NAME }}" >> $GITHUB_OUTPUT 86 | echo "digest=${{ steps.build-image.outputs.digest }}" >> $GITHUB_OUTPUT 87 | -------------------------------------------------------------------------------- /api/v1alpha1/shim_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // ShimSpec defines the desired state of Shim 24 | type ShimSpec struct { 25 | NodeSelector map[string]string `json:"nodeSelector,omitempty"` 26 | FetchStrategy FetchStrategy `json:"fetchStrategy"` 27 | RuntimeClass RuntimeClassSpec `json:"runtimeClass"` 28 | RolloutStrategy RolloutStrategy `json:"rolloutStrategy"` 29 | // ContainerdRuntimeOptions is a map of containerd runtime options for the shim plugin. 30 | // See an example of configuring cgroup driver via runtime options: https://github.com/containerd/containerd/blob/main/docs/cri/config.md#cgroup-driver 31 | ContainerdRuntimeOptions map[string]string `json:"containerdRuntimeOptions,omitempty"` 32 | } 33 | 34 | type FetchStrategy struct { 35 | Type string `json:"type"` 36 | AnonHTTP AnonHTTPSpec `json:"anonHttp"` 37 | } 38 | 39 | type AnonHTTPSpec struct { 40 | Location string `json:"location"` 41 | } 42 | 43 | type RuntimeClassSpec struct { 44 | Name string `json:"name"` 45 | Handler string `json:"handler"` 46 | } 47 | 48 | // +kubebuilder:validation:Enum=rolling;recreate 49 | type RolloutStrategyType string 50 | 51 | const ( 52 | RolloutStrategyTypeRolling RolloutStrategyType = "rolling" 53 | RolloutStrategyTypeRecreate RolloutStrategyType = "recreate" 54 | ) 55 | 56 | type RolloutStrategy struct { 57 | Type RolloutStrategyType `json:"type"` 58 | Rolling RollingSpec `json:"rolling,omitempty"` 59 | } 60 | 61 | type RollingSpec struct { 62 | MaxUpdate int `json:"maxUpdate"` 63 | } 64 | 65 | // ShimStatus defines the observed state of Shim 66 | // +operator-sdk:csv:customresourcedefinitions:type=status 67 | type ShimStatus struct { 68 | Conditions []metav1.Condition `json:"conditions,omitempty"` 69 | NodeCount int `json:"nodes"` 70 | NodeReadyCount int `json:"nodesReady"` 71 | } 72 | 73 | // +kubebuilder:object:root=true 74 | // +kubebuilder:resource:path=shims,scope=Cluster 75 | // +kubebuilder:printcolumn:JSONPath=".spec.runtimeClass.name",name=RuntimeClass,type=string 76 | // +kubebuilder:printcolumn:JSONPath=".status.nodesReady",name=Ready,type=integer 77 | // +kubebuilder:printcolumn:JSONPath=".status.nodes",name=Nodes,type=integer 78 | // Shim is the Schema for the shims API 79 | type Shim struct { 80 | metav1.TypeMeta `json:",inline"` 81 | metav1.ObjectMeta `json:"metadata,omitempty"` 82 | 83 | Spec ShimSpec `json:"spec,omitempty"` 84 | Status ShimStatus `json:"status,omitempty"` 85 | } 86 | 87 | // +kubebuilder:object:root=true 88 | 89 | // ShimList contains a list of Shim 90 | type ShimList struct { 91 | metav1.TypeMeta `json:",inline"` 92 | metav1.ListMeta `json:"metadata,omitempty"` 93 | Items []Shim `json:"items"` 94 | } 95 | 96 | func init() { 97 | SchemeBuilder.Register(&Shim{}, &ShimList{}) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/node-installer/install_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/spf13/afero" 23 | main "github.com/spinframework/runtime-class-manager/cmd/node-installer" 24 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | type nullRestarter struct{} 29 | 30 | func (n nullRestarter) Restart() error { 31 | return nil 32 | } 33 | 34 | func Test_RunInstall(t *testing.T) { 35 | type args struct { 36 | config main.Config 37 | rootFs afero.Fs 38 | hostFs afero.Fs 39 | } 40 | tests := []struct { 41 | name string 42 | args args 43 | wantErr bool 44 | }{ 45 | { 46 | "new shim", 47 | args{ 48 | main.Config{ 49 | struct { 50 | Name string 51 | ConfigPath string 52 | Options map[string]string 53 | }{"containerd", "/etc/containerd/config.toml", nil}, 54 | struct { 55 | Path string 56 | AssetPath string 57 | }{"/opt/rcm", "/assets"}, 58 | struct{ RootPath string }{"/containerd/missing-containerd-shim-config"}, 59 | }, 60 | tests.FixtureFs("../../testdata/node-installer"), 61 | tests.FixtureFs("../../testdata/node-installer/containerd/missing-containerd-shim-config"), 62 | }, 63 | false, 64 | }, 65 | { 66 | "existing shim", 67 | args{ 68 | main.Config{ 69 | struct { 70 | Name string 71 | ConfigPath string 72 | Options map[string]string 73 | }{"containerd", "/etc/containerd/config.toml", nil}, 74 | struct { 75 | Path string 76 | AssetPath string 77 | }{"/opt/rcm", "/assets"}, 78 | struct{ RootPath string }{"/containerd/existing-containerd-shim-config"}, 79 | }, 80 | tests.FixtureFs("../../testdata/node-installer"), 81 | tests.FixtureFs("../../testdata/node-installer/containerd/existing-containerd-shim-config"), 82 | }, 83 | false, 84 | }, 85 | { 86 | // TODO figure out how to test that the runtime options are set in the config 87 | "new shim with runtime options", 88 | args{ 89 | main.Config{ 90 | struct { 91 | Name string 92 | ConfigPath string 93 | Options map[string]string 94 | }{"containerd", "/etc/containerd/config.toml", map[string]string{"SystemdCgroup": "true"}}, 95 | struct { 96 | Path string 97 | AssetPath string 98 | }{"/opt/rcm", "/assets"}, 99 | struct{ RootPath string }{"/containerd/missing-containerd-shim-config"}, 100 | }, 101 | tests.FixtureFs("../../testdata/node-installer"), 102 | tests.FixtureFs("../../testdata/node-installer/containerd/missing-containerd-shim-config"), 103 | }, 104 | false, 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | err := main.RunInstall(tt.args.config, tt.args.rootFs, tt.args.hostFs, nullRestarter{}) 110 | if tt.wantErr { 111 | require.Error(t, err) 112 | } else { 113 | require.NoError(t, err) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/shim/install_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shim //nolint:testpackage // whitebox test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/spf13/afero" 23 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestConfig_Install(t *testing.T) { 29 | type wants struct { 30 | filepath string 31 | changed bool 32 | } 33 | type fields struct { 34 | rootFs afero.Fs 35 | hostFs afero.Fs 36 | assetPath string 37 | rcmPath string 38 | } 39 | type args struct { 40 | shimName string 41 | } 42 | tests := []struct { 43 | name string 44 | fields fields 45 | args args 46 | want wants 47 | wantErr bool 48 | }{ 49 | { 50 | "successful shim installation", 51 | fields{ 52 | tests.FixtureFs("../../testdata/node-installer"), 53 | afero.NewMemMapFs(), 54 | "/assets", 55 | "/opt/rcm", 56 | }, 57 | args{"containerd-shim-slight-v1"}, 58 | wants{ 59 | "/opt/rcm/bin/containerd-shim-slight-v1", 60 | true, 61 | }, 62 | false, 63 | }, 64 | { 65 | "no changes to shim", 66 | fields{ 67 | tests.FixtureFs("../../testdata/node-installer"), 68 | tests.FixtureFs("../../testdata/node-installer/shim"), 69 | "/assets", 70 | "/opt/rcm", 71 | }, 72 | args{"containerd-shim-spin-v1"}, 73 | wants{ 74 | "/opt/rcm/bin/containerd-shim-spin-v1", 75 | false, 76 | }, 77 | false, 78 | }, 79 | { 80 | "install new shim over old", 81 | fields{ 82 | tests.FixtureFs("../../testdata/node-installer"), 83 | tests.FixtureFs("../../testdata/node-installer/shim"), 84 | "/assets", 85 | "/opt/rcm", 86 | }, 87 | args{"containerd-shim-slight-v1"}, 88 | wants{ 89 | "/opt/rcm/bin/containerd-shim-slight-v1", 90 | true, 91 | }, 92 | false, 93 | }, 94 | { 95 | "unable to find new shim", 96 | fields{ 97 | afero.NewMemMapFs(), 98 | tests.FixtureFs("../../testdata/node-installer/shim"), 99 | "/assets", 100 | "/opt/rcm", 101 | }, 102 | args{"some-shim"}, 103 | wants{ 104 | "", 105 | false, 106 | }, 107 | true, 108 | }, 109 | { 110 | "unable to write to hostFs", 111 | fields{ 112 | tests.FixtureFs("../../testdata/node-installer"), 113 | afero.NewReadOnlyFs(tests.FixtureFs("../../testdata/node-installer/shim")), 114 | "/assets", 115 | "/opt/rcm", 116 | }, 117 | args{"containerd-shim-spin-v1"}, 118 | wants{ 119 | "/opt/rcm/bin/containerd-shim-spin-v1", 120 | false, 121 | }, 122 | true, 123 | }, 124 | } 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | c := &Config{ 128 | rootFs: tt.fields.rootFs, 129 | hostFs: tt.fields.hostFs, 130 | assetPath: tt.fields.assetPath, 131 | rcmPath: tt.fields.rcmPath, 132 | } 133 | 134 | filepath, changed, err := c.Install(tt.args.shimName) 135 | 136 | if tt.wantErr { 137 | require.Error(t, err) 138 | } else { 139 | require.NoError(t, err) 140 | } 141 | 142 | assert.Equal(t, tt.want.filepath, filepath) 143 | assert.Equal(t, tt.want.changed, changed) 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /internal/state/state_test.go: -------------------------------------------------------------------------------- 1 | package state_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/afero" 7 | "github.com/spinframework/runtime-class-manager/internal/state" 8 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGet(t *testing.T) { 14 | type args struct { 15 | fs afero.Fs 16 | rcmPath string 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | want *state.State 22 | wantErr bool 23 | }{ 24 | { 25 | "existing state", 26 | args{ 27 | tests.FixtureFs("../../testdata/node-installer/containerd/existing-containerd-shim-config"), 28 | "/opt/rcm", 29 | }, 30 | &state.State{ 31 | Shims: map[string]*state.Shim{ 32 | "spin-v1": { 33 | Sha256: []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 34 | Path: "/opt/rcm/bin/containerd-shim-spin-v1", 35 | }, 36 | }, 37 | }, 38 | false, 39 | }, 40 | { 41 | "missing state", 42 | args{ 43 | tests.FixtureFs("../../testdata/node-installer/containerd/missing-containerd-shim-config"), 44 | "/opt/rcm", 45 | }, 46 | &state.State{ 47 | Shims: map[string]*state.Shim{}, 48 | }, 49 | false, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | got, err := state.Get(tt.args.fs, tt.args.rcmPath) 55 | if tt.wantErr { 56 | require.Error(t, err) 57 | } else { 58 | require.NoError(t, err) 59 | } 60 | assert.Equal(t, tt.want.Shims, got.Shims) 61 | }) 62 | } 63 | } 64 | 65 | func TestShimChanged(t *testing.T) { 66 | type args struct { 67 | shimName string 68 | sha256 []byte 69 | path string 70 | } 71 | state := &state.State{ 72 | Shims: map[string]*state.Shim{ 73 | "spin-v1": { 74 | Sha256: []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 75 | Path: "/opt/rcm/bin/containerd-shim-spin-v1", 76 | }, 77 | }, 78 | } 79 | 80 | tests := []struct { 81 | name string 82 | args args 83 | want bool 84 | }{ 85 | { 86 | "existing shim, same sha256 and path", 87 | args{ 88 | "spin-v1", 89 | []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 90 | "/opt/rcm/bin/containerd-shim-spin-v1", 91 | }, 92 | false, 93 | }, 94 | { 95 | "existing shim, different sha256", 96 | args{ 97 | "spin-v1", 98 | []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 83}, 99 | "/opt/rcm/bin/containerd-shim-spin-v1", 100 | }, 101 | true, 102 | }, 103 | { 104 | "existing shim, different path", 105 | args{ 106 | "spin-v1", 107 | []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 108 | "/opt/rcm/bin/containerd-shim-spin-v2", 109 | }, 110 | true, 111 | }, 112 | { 113 | "non-existing shim", 114 | args{ 115 | "non-existing", 116 | []byte{109, 165, 232, 241, 122, 155, 250, 156, 176, 76, 242, 44, 135, 182, 71, 83, 148, 236, 236, 58, 244, 253, 195, 55, 247, 45, 109, 191, 51, 25, 234, 82}, 117 | "/opt/rcm/bin/containerd-shim-spin-v1", 118 | }, 119 | true, 120 | }, 121 | } 122 | 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | changed := state.ShimChanged(tt.args.shimName, tt.args.sha256, tt.args.path) 126 | assert.Equal(t, tt.want, changed) 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: runtime-class-manager 10 | app.kubernetes.io/part-of: runtime-class-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: runtime-class-manager 25 | app.kubernetes.io/part-of: runtime-class-manager 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spinframework/runtime-class-manager 2 | 3 | go 1.25.5 4 | 5 | require ( 6 | github.com/mitchellh/go-ps v1.0.0 7 | github.com/onsi/ginkgo/v2 v2.27.3 8 | github.com/onsi/gomega v1.38.3 9 | github.com/prometheus/common v0.67.4 10 | github.com/rs/zerolog v1.34.0 11 | github.com/spf13/afero v1.15.0 12 | github.com/spf13/cobra v1.10.2 13 | github.com/spf13/pflag v1.0.10 14 | github.com/spf13/viper v1.21.0 15 | github.com/stretchr/testify v1.11.1 16 | k8s.io/api v0.34.3 17 | k8s.io/apimachinery v0.34.3 18 | k8s.io/client-go v0.34.3 19 | sigs.k8s.io/controller-runtime v0.22.4 20 | ) 21 | 22 | require ( 23 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 28 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 29 | github.com/fsnotify/fsnotify v1.9.0 // indirect 30 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 31 | github.com/go-logr/logr v1.4.3 // indirect 32 | github.com/go-logr/zapr v1.3.0 // indirect 33 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 34 | github.com/go-openapi/jsonreference v0.20.2 // indirect 35 | github.com/go-openapi/swag v0.23.0 // indirect 36 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 37 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/google/btree v1.1.3 // indirect 40 | github.com/google/gnostic-models v0.7.0 // indirect 41 | github.com/google/go-cmp v0.7.0 // indirect 42 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.19 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 52 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 53 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 56 | github.com/prometheus/client_golang v1.22.0 // indirect 57 | github.com/prometheus/client_model v0.6.2 // indirect 58 | github.com/prometheus/procfs v0.15.1 // indirect 59 | github.com/sagikazarmark/locafero v0.11.0 // indirect 60 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 61 | github.com/spf13/cast v1.10.0 // indirect 62 | github.com/subosito/gotenv v1.6.0 // indirect 63 | github.com/x448/float16 v0.8.4 // indirect 64 | go.uber.org/multierr v1.11.0 // indirect 65 | go.uber.org/zap v1.27.0 // indirect 66 | go.yaml.in/yaml/v2 v2.4.3 // indirect 67 | go.yaml.in/yaml/v3 v3.0.4 // indirect 68 | golang.org/x/mod v0.28.0 // indirect 69 | golang.org/x/net v0.46.0 // indirect 70 | golang.org/x/oauth2 v0.32.0 // indirect 71 | golang.org/x/sync v0.17.0 // indirect 72 | golang.org/x/sys v0.37.0 // indirect 73 | golang.org/x/term v0.36.0 // indirect 74 | golang.org/x/text v0.30.0 // indirect 75 | golang.org/x/time v0.9.0 // indirect 76 | golang.org/x/tools v0.37.0 // indirect 77 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 78 | google.golang.org/protobuf v1.36.10 // indirect 79 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 80 | gopkg.in/inf.v0 v0.9.1 // indirect 81 | gopkg.in/yaml.v3 v3.0.1 // indirect 82 | k8s.io/apiextensions-apiserver v0.34.1 // indirect 83 | k8s.io/klog/v2 v2.130.1 // indirect 84 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 85 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 86 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 87 | sigs.k8s.io/randfill v1.0.0 // indirect 88 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 89 | sigs.k8s.io/yaml v1.6.0 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runtime-class-manager 2 | 3 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/spinframework/runtime-class-manager/badge)](https://securityscorecards.dev/viewer/?uri=github.com/spinframework/runtime-class-manager) 4 | 5 | The runtime-class-manager is the spiritual successor to the kwasm-operator. kwasm has been developed as an experimental, simple way to install Wasm runtimes. This experiment has been relatively successful, as more and more users utilized it to fiddle around with Wasm on Kubernetes. However, the kwasm-operator has some limitations that make it difficult to use in production. The runtime-class-manager is an attempt to address these limitations to make it a reliable and secure way to deploy arbitrary containerd shims. 6 | 7 | The implementation of runtime-class-manager follows [this](https://hackmd.io/TwC8Fc8wTCKdoWlgNOqTgA) community proposal. 8 | 9 | The name should be treated as a working title and is hopefully subject to change. 10 | 11 | ## Roadmap 12 | 13 | For the 1.0 release of runtime-class-manager, we consider three milestones: 14 | 15 | - **M1: [RCM MVP for Spinkube](https://github.com/spinframework/runtime-class-manager/milestone/1)** 16 | This milestone is about getting RCM to a state where Spinkube can rely on RCM and use it as a dependency instead of Kwasm. This means, that the focus is on managing lifecycle of [containerd-shim-spin](https://github.com/spinframework/containerd-shim-spin) on nodes. 17 | - **M2: [Kwasm Feature Parity](https://github.com/spinframework/runtime-class-manager/milestone/2)** 18 | All shims that kwasm can install, should be installable via rcm. Automated tests are in place to ensure installation of RCM and shims that are supported by Kwasm. 19 | - **M3: [Full implementation of the initial spec](https://github.com/spinframework/runtime-class-manager/milestone/3)** 20 | Stable spec of the Shim CRD based on the [initial proposal](https://hackmd.io/TwC8Fc8wTCKdoWlgNOqTgA). After 1.0 we assume no breaking changes of the Shim CRD. Arbitrary shims can be installed via RCM and prominent shims are tested automatically, on various Kubernetes distributions. 21 | - Future (ideas): 22 | - support for additional container runtimes, like CRI-O to enable RCM on OpenShift 23 | - alternative shim installation via Daemonset instead of Jobs 24 | - treating node-installer as a daemon process, to enable better conflict resolution 25 | 26 | ## Usage 27 | 28 | ### To Deploy on the cluster 29 | 30 | **Build and push your image to the location specified by `IMG`:** 31 | 32 | ```sh 33 | make docker-build docker-push IMG=/runtime-class-manager:tag 34 | ``` 35 | 36 | **NOTE:** This image ought to be published in the personal registry you specified. 37 | And it is required to have access to pull the image from the working environment. 38 | Make sure you have the proper permission to the registry if the above commands don’t work. 39 | 40 | **Install the CRDs into the cluster:** 41 | 42 | ```sh 43 | make install 44 | ``` 45 | 46 | **Deploy the Manager to the cluster with the image specified by `IMG`:** 47 | 48 | ```sh 49 | make deploy IMG=/runtime-class-manager:tag 50 | ``` 51 | 52 | > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin 53 | privileges or be logged in as admin. 54 | 55 | **Create instances of your solution** 56 | You can apply the samples (examples) from the config/sample: 57 | 58 | ```sh 59 | kubectl apply -k config/samples/ 60 | ``` 61 | 62 | >**NOTE**: Ensure that the samples has default values to test it out. 63 | 64 | ### To Uninstall 65 | 66 | **Delete the instances (CRs) from the cluster:** 67 | 68 | ```sh 69 | kubectl delete -k config/samples/ 70 | ``` 71 | 72 | **Delete the APIs(CRDs) from the cluster:** 73 | 74 | ```sh 75 | make uninstall 76 | ``` 77 | 78 | **UnDeploy the controller from the cluster:** 79 | 80 | ```sh 81 | make undeploy 82 | ``` 83 | 84 | ## License 85 | 86 | Copyright 2024. 87 | 88 | Licensed under the Apache License, Version 2.0 (the "License"); 89 | you may not use this file except in compliance with the License. 90 | You may obtain a copy of the License at 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | -------------------------------------------------------------------------------- /internal/containerd/configure.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package containerd 18 | 19 | import ( 20 | "fmt" 21 | "log/slog" 22 | "os" 23 | "path" 24 | "strings" 25 | 26 | "github.com/spf13/afero" 27 | "github.com/spinframework/runtime-class-manager/internal/shim" 28 | ) 29 | 30 | type Restarter interface { 31 | Restart() error 32 | } 33 | 34 | type Config struct { 35 | hostFs afero.Fs 36 | configPath string 37 | restarter Restarter 38 | runtimeOptions map[string]string 39 | } 40 | 41 | func NewConfig(hostFs afero.Fs, configPath string, restarter Restarter, runtimeOptions map[string]string) *Config { 42 | return &Config{ 43 | hostFs: hostFs, 44 | configPath: configPath, 45 | restarter: restarter, 46 | runtimeOptions: runtimeOptions, 47 | } 48 | } 49 | 50 | func (c *Config) AddRuntime(shimPath string) error { 51 | runtimeName := shim.RuntimeName(path.Base(shimPath)) 52 | l := slog.With("runtime", runtimeName) 53 | 54 | // Containerd config file needs to exist, otherwise return the error 55 | data, err := afero.ReadFile(c.hostFs, c.configPath) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | // Warn if config.toml already contains runtimeName 61 | if strings.Contains(string(data), runtimeName) { 62 | l.Info("runtime config already exists, skipping") 63 | return nil 64 | } 65 | 66 | cfg := generateConfig(shimPath, runtimeName, c.runtimeOptions, data) 67 | 68 | // Open file in append mode 69 | file, err := c.hostFs.OpenFile(c.configPath, os.O_APPEND|os.O_WRONLY, 0o644) //nolint:mnd // file permissions 70 | if err != nil { 71 | return err 72 | } 73 | defer file.Close() 74 | 75 | // Append config 76 | _, err = file.WriteString(cfg) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (c *Config) RemoveRuntime(shimPath string) (changed bool, err error) { 85 | runtimeName := shim.RuntimeName(path.Base(shimPath)) 86 | l := slog.With("runtime", runtimeName) 87 | 88 | // Containerd config file needs to exist, otherwise return the error 89 | data, err := afero.ReadFile(c.hostFs, c.configPath) 90 | if err != nil { 91 | return false, err 92 | } 93 | 94 | // Warn if config.toml does not contain the runtimeName 95 | if !strings.Contains(string(data), runtimeName) { 96 | l.Warn("runtime config does not exist, skipping") 97 | return false, nil 98 | } 99 | 100 | cfg := generateConfig(shimPath, runtimeName, c.runtimeOptions, data) 101 | 102 | // Convert the file data to a string and replace the target string with an empty string. 103 | modifiedData := strings.ReplaceAll(string(data), cfg, "") 104 | 105 | // Write the modified data back to the file. 106 | err = afero.WriteFile(c.hostFs, c.configPath, []byte(modifiedData), 0o644) //nolint:mnd // file permissions 107 | if err != nil { 108 | return false, err 109 | } 110 | 111 | return true, nil 112 | } 113 | 114 | func (c *Config) RestartRuntime() error { 115 | return c.restarter.Restart() 116 | } 117 | 118 | func generateConfig(shimPath string, runtimeName string, runtimeOptions map[string]string, configData []byte) string { 119 | // Config domain for containerd 1.0 (config version 2) 120 | domain := "io.containerd.grpc.v1.cri" 121 | if strings.Contains(string(configData), "version = 3") { 122 | // Config domain for containerd 2.0 (config version 3) 123 | domain = "io.containerd.cri.v1.runtime" 124 | } 125 | 126 | runtimeConfiguration := fmt.Sprintf(` 127 | # RCM runtime config for %s 128 | [plugins."%s".containerd.runtimes.%s] 129 | runtime_type = "%s" 130 | `, runtimeName, domain, runtimeName, shimPath) 131 | // Add runtime options if any are provided 132 | if len(runtimeOptions) > 0 { 133 | options := fmt.Sprintf(`[plugins."%s".containerd.runtimes.%s.options]`, domain, runtimeName) 134 | for k, v := range runtimeOptions { 135 | options += fmt.Sprintf(` 136 | %s = %s`, k, v) 137 | } 138 | runtimeConfiguration += options 139 | } 140 | return runtimeConfiguration 141 | } 142 | -------------------------------------------------------------------------------- /cmd/node-installer/install.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The KWasm Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/fs" 23 | "log/slog" 24 | "os" 25 | "path" 26 | 27 | "github.com/spf13/afero" 28 | "github.com/spf13/cobra" 29 | "github.com/spinframework/runtime-class-manager/internal/containerd" 30 | "github.com/spinframework/runtime-class-manager/internal/preset" 31 | "github.com/spinframework/runtime-class-manager/internal/shim" 32 | ) 33 | 34 | // installCmd represents the install command. 35 | var installCmd = &cobra.Command{ 36 | Use: "install", 37 | Short: "Install containerd shims", 38 | Run: func(_ *cobra.Command, _ []string) { 39 | rootFs := afero.NewOsFs() 40 | hostFs := afero.NewBasePathFs(rootFs, config.Host.RootPath) 41 | 42 | distro, err := DetectDistro(config, hostFs) 43 | if err != nil { 44 | slog.Error("failed to detect containerd config", "error", err) 45 | os.Exit(1) 46 | } 47 | 48 | config.Runtime.ConfigPath = distro.ConfigPath 49 | if err = distro.Setup(preset.Env{ConfigPath: distro.ConfigPath, HostFs: hostFs}); err != nil { 50 | slog.Error("failed to run distro setup", "error", err) 51 | os.Exit(1) 52 | } 53 | 54 | config.Runtime.Options, err = RuntimeOptions() 55 | if err != nil { 56 | slog.Error("failed to get runtime options", "error", err) 57 | os.Exit(1) 58 | } 59 | 60 | if err := RunInstall(config, rootFs, hostFs, distro.Restarter); err != nil { 61 | slog.Error("failed to install", "error", err) 62 | os.Exit(1) 63 | } 64 | }, 65 | } 66 | 67 | func init() { 68 | installCmd.Flags().StringVarP(&config.RCM.AssetPath, "asset-path", "a", "/assets", "Path to the asset to install") 69 | rootCmd.AddCommand(installCmd) 70 | } 71 | 72 | func RunInstall(config Config, rootFs, hostFs afero.Fs, restarter containerd.Restarter) error { 73 | // Get file or directory information. 74 | info, err := rootFs.Stat(config.RCM.AssetPath) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | var files []fs.FileInfo 80 | // Check if the path is a directory. 81 | if info.IsDir() { 82 | files, err = afero.ReadDir(rootFs, config.RCM.AssetPath) 83 | if err != nil { 84 | return err 85 | } 86 | } else { 87 | // If the path is not a directory, add the file to the list of files. 88 | files = append(files, info) 89 | config.RCM.AssetPath = path.Dir(config.RCM.AssetPath) 90 | } 91 | 92 | containerdConfig := containerd.NewConfig(hostFs, config.Runtime.ConfigPath, restarter, config.Runtime.Options) 93 | shimConfig := shim.NewConfig(rootFs, hostFs, config.RCM.AssetPath, config.RCM.Path) 94 | 95 | anythingChanged := false 96 | for _, file := range files { 97 | fileName := file.Name() 98 | runtimeName := shim.RuntimeName(fileName) 99 | 100 | binPath, changed, err := shimConfig.Install(fileName) 101 | if err != nil { 102 | return fmt.Errorf("failed to install shim '%s': %w", runtimeName, err) 103 | } 104 | anythingChanged = anythingChanged || changed 105 | slog.Info("shim installed", "shim", runtimeName, "path", binPath, "new-version", changed) 106 | 107 | err = containerdConfig.AddRuntime(binPath) 108 | if err != nil { 109 | return fmt.Errorf("failed to write containerd config: %w", err) 110 | } 111 | slog.Info("shim configured", "shim", runtimeName, "path", config.Runtime.ConfigPath) 112 | } 113 | 114 | if !anythingChanged { 115 | slog.Info("nothing changed, nothing more to do") 116 | return nil 117 | } 118 | 119 | // Ensure D-Bus is installed and running if using systemd 120 | if _, err := containerd.ListSystemdUnits(); err == nil { 121 | err = containerd.InstallDbus() 122 | if err != nil { 123 | return fmt.Errorf("failed to install D-Bus: %w", err) 124 | } 125 | } 126 | 127 | slog.Info("restarting containerd") 128 | err = containerdConfig.RestartRuntime() 129 | if err != nil { 130 | return fmt.Errorf("failed to restart containerd: %w", err) 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func RuntimeOptions() (map[string]string, error) { 137 | runtimeOptions := make(map[string]string) 138 | optionsJSON := os.Getenv("RUNTIME_OPTIONS") 139 | config.Runtime.Options = make(map[string]string) 140 | if optionsJSON != "" { 141 | err := json.Unmarshal([]byte(optionsJSON), &runtimeOptions) 142 | if err != nil { 143 | return nil, fmt.Errorf("failed to unmarshal runtime options JSON %s: %w", optionsJSON, err) 144 | } 145 | } 146 | return runtimeOptions, nil 147 | } 148 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - asasalint 6 | - asciicheck 7 | - bidichk 8 | - copyloopvar 9 | - cyclop 10 | - dupl 11 | - errcheck 12 | - errname 13 | - errorlint 14 | - forbidigo 15 | - funlen 16 | - gocognit 17 | - goconst 18 | - gocritic 19 | - gocyclo 20 | - goprintffuncname 21 | - gosec 22 | - govet 23 | - ineffassign 24 | - makezero 25 | - mnd 26 | - nakedret 27 | - nestif 28 | - nilerr 29 | - nilnil 30 | - noctx 31 | - nolintlint 32 | - predeclared 33 | - promlinter 34 | - reassign 35 | - revive 36 | - sloglint 37 | - staticcheck 38 | - testableexamples 39 | - testifylint 40 | - testpackage 41 | - tparallel 42 | - unconvert 43 | - unparam 44 | - unused 45 | - usestdlibvars 46 | - usetesting 47 | - wastedassign 48 | - whitespace 49 | settings: 50 | cyclop: 51 | max-complexity: 30 52 | package-average: 10 53 | errcheck: 54 | check-type-assertions: true 55 | exhaustive: 56 | check: 57 | - switch 58 | - map 59 | exhaustruct: 60 | exclude: 61 | - ^net/http.Client$ 62 | - ^net/http.Cookie$ 63 | - ^net/http.Request$ 64 | - ^net/http.Response$ 65 | - ^net/http.Server$ 66 | - ^net/http.Transport$ 67 | - ^net/url.URL$ 68 | - ^os/exec.Cmd$ 69 | - ^reflect.StructField$ 70 | - ^github.com/Shopify/sarama.Config$ 71 | - ^github.com/Shopify/sarama.ProducerMessage$ 72 | - ^github.com/mitchellh/mapstructure.DecoderConfig$ 73 | - ^github.com/prometheus/client_golang/.+Opts$ 74 | - ^github.com/spf13/cobra.Command$ 75 | - ^github.com/spf13/cobra.CompletionOptions$ 76 | - ^github.com/stretchr/testify/mock.Mock$ 77 | - ^github.com/testcontainers/testcontainers-go.+Request$ 78 | - ^github.com/testcontainers/testcontainers-go.FromDockerfile$ 79 | - ^golang.org/x/tools/go/analysis.Analyzer$ 80 | - ^google.golang.org/protobuf/.+Options$ 81 | - ^gopkg.in/yaml.v3.Node$ 82 | funlen: 83 | lines: 100 84 | statements: 50 85 | ignore-comments: true 86 | gocognit: 87 | min-complexity: 20 88 | gocritic: 89 | settings: 90 | captLocal: 91 | paramsOnly: false 92 | underef: 93 | skipRecvDeref: false 94 | govet: 95 | disable: 96 | - fieldalignment 97 | - shadow 98 | enable-all: true 99 | inamedparam: 100 | skip-single-param: true 101 | mnd: 102 | ignored-functions: 103 | - flag.Arg 104 | - flag.Duration.* 105 | - flag.Float.* 106 | - flag.Int.* 107 | - flag.Uint.* 108 | - os.Chmod 109 | - os.Mkdir.* 110 | - os.OpenFile 111 | - os.WriteFile 112 | - prometheus.ExponentialBuckets.* 113 | - prometheus.LinearBuckets 114 | - math.Min 115 | nakedret: 116 | max-func-lines: 0 117 | nestif: 118 | min-complexity: 8 119 | nolintlint: 120 | require-explanation: true 121 | require-specific: true 122 | allow-no-explanation: 123 | - funlen 124 | - gocognit 125 | - lll 126 | rowserrcheck: 127 | packages: 128 | - github.com/jmoiron/sqlx 129 | wrapcheck: 130 | ignore-sigs: 131 | - .Complete( 132 | - client.IgnoreNotFound( 133 | - fmt.Errorf( 134 | exclusions: 135 | generated: lax 136 | presets: 137 | - comments 138 | - common-false-positives 139 | - legacy 140 | - std-error-handling 141 | rules: 142 | - linters: 143 | - godot 144 | source: (noinspection|TODO) 145 | - linters: 146 | - gocritic 147 | source: //noinspection 148 | - linters: 149 | - bodyclose 150 | - dupl 151 | - funlen 152 | - goconst 153 | - gosec 154 | - noctx 155 | - wrapcheck 156 | path: _test\.go 157 | - linters: 158 | - funlen 159 | source: ^func Test 160 | - linters: 161 | - revive 162 | path: (.+)_test\.go 163 | text: 'dot-imports: should not use dot imports' 164 | - linters: 165 | - unparam 166 | path: internal/controller/shim_controller.go 167 | source: ctrl.Result 168 | - linters: 169 | - revive 170 | path: pkg/containerd/restart_unix.go 171 | text: 'exported: type name will be used as containerd.ContainerdRestarter by other packages, and that stutters; consider calling this Restarter' 172 | paths: 173 | - third_party$ 174 | - builtin$ 175 | - examples$ 176 | issues: 177 | max-same-issues: 30 178 | formatters: 179 | enable: 180 | - goimports 181 | exclusions: 182 | generated: lax 183 | paths: 184 | - third_party$ 185 | - builtin$ 186 | - examples$ 187 | -------------------------------------------------------------------------------- /cmd/rcm/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | "github.com/prometheus/common/version" 26 | _ "k8s.io/client-go/plugin/pkg/client/auth" 27 | 28 | "k8s.io/apimachinery/pkg/runtime" 29 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 30 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/healthz" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 35 | 36 | runtimev1alpha1 "github.com/spinframework/runtime-class-manager/api/v1alpha1" 37 | "github.com/spinframework/runtime-class-manager/internal/controller" 38 | //+kubebuilder:scaffold:imports 39 | ) 40 | 41 | var ( 42 | scheme = runtime.NewScheme() 43 | setupLog = ctrl.Log.WithName("setup") 44 | ) 45 | 46 | func init() { 47 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 48 | 49 | utilruntime.Must(runtimev1alpha1.AddToScheme(scheme)) 50 | //+kubebuilder:scaffold:scheme 51 | } 52 | 53 | func main() { 54 | var metricsAddr string 55 | var enableLeaderElection bool 56 | var probeAddr string 57 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 58 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8082", "The address the probe endpoint binds to.") 59 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 60 | "Enable leader election for controller manager. "+ 61 | "Enabling this will ensure there is only one active controller manager.") 62 | opts := zap.Options{ 63 | Development: true, 64 | } 65 | opts.BindFlags(flag.CommandLine) 66 | flag.Parse() 67 | 68 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 69 | 70 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 71 | Scheme: scheme, 72 | Metrics: metricsserver.Options{BindAddress: metricsAddr}, 73 | HealthProbeBindAddress: probeAddr, 74 | LeaderElection: enableLeaderElection, 75 | LeaderElectionID: "7584d300.spinkube.dev", 76 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 77 | // when the Manager ends. This requires the binary to immediately end when the 78 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 79 | // speeds up voluntary leader transitions as the new leader don't have to wait 80 | // LeaseDuration time first. 81 | // 82 | // In the default scaffold provided, the program ends immediately after 83 | // the manager stops, so would be fine to enable this option. However, 84 | // if you are doing or is intended to do any operation such as perform cleanups 85 | // after the manager stops then its usage might be unsafe. 86 | // LeaderElectionReleaseOnCancel: true, 87 | }) 88 | if err != nil { 89 | setupLog.Error(err, "unable to start manager") 90 | os.Exit(1) 91 | } 92 | 93 | if err = (&controller.ShimReconciler{ 94 | Client: mgr.GetClient(), 95 | Scheme: mgr.GetScheme(), 96 | }).SetupWithManager(mgr); err != nil { 97 | setupLog.Error(err, "unable to create controller", "controller", "Shim") 98 | os.Exit(1) 99 | } 100 | // if err = (&controller.NodeReconciler{ 101 | // Client: mgr.GetClient(), 102 | // Scheme: mgr.GetScheme(), 103 | // }).SetupWithManager(mgr); err != nil { 104 | // setupLog.Error(err, "unable to create controller", "controller", "Node") 105 | // os.Exit(1) 106 | // } 107 | if err = (&controller.JobReconciler{ 108 | Client: mgr.GetClient(), 109 | Scheme: mgr.GetScheme(), 110 | }).SetupWithManager(mgr); err != nil { 111 | setupLog.Error(err, "unable to create controller", "controller", "Job") 112 | os.Exit(1) 113 | } 114 | //+kubebuilder:scaffold:builder 115 | 116 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 117 | setupLog.Error(err, "unable to set up health check") 118 | os.Exit(1) 119 | } 120 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 121 | setupLog.Error(err, "unable to set up ready check") 122 | os.Exit(1) 123 | } 124 | 125 | setupLog.Info("starting manager") 126 | setupLog.Info("version", "version", version.Version, "branch", version.Branch, "revision", version.Revision, "builddate", version.BuildDate) 127 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 128 | setupLog.Error(err, "problem running manager") 129 | os.Exit(1) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: runtime-class-manager-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: runtime-class-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patches: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | - path: manager_auth_proxy_patch.yaml 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | #- path: manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- path: webhookcainjection_patch.yaml 43 | 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | # Uncomment the following replacements to add the cert-manager CA injection annotations 46 | #replacements: 47 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 48 | # kind: Certificate 49 | # group: cert-manager.io 50 | # version: v1 51 | # name: serving-cert # this name should match the one in certificate.yaml 52 | # fieldPath: .metadata.namespace # namespace of the certificate CR 53 | # targets: 54 | # - select: 55 | # kind: ValidatingWebhookConfiguration 56 | # fieldPaths: 57 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 58 | # options: 59 | # delimiter: '/' 60 | # index: 0 61 | # create: true 62 | # - select: 63 | # kind: MutatingWebhookConfiguration 64 | # fieldPaths: 65 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 66 | # options: 67 | # delimiter: '/' 68 | # index: 0 69 | # create: true 70 | # - select: 71 | # kind: CustomResourceDefinition 72 | # fieldPaths: 73 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 74 | # options: 75 | # delimiter: '/' 76 | # index: 0 77 | # create: true 78 | # - source: 79 | # kind: Certificate 80 | # group: cert-manager.io 81 | # version: v1 82 | # name: serving-cert # this name should match the one in certificate.yaml 83 | # fieldPath: .metadata.name 84 | # targets: 85 | # - select: 86 | # kind: ValidatingWebhookConfiguration 87 | # fieldPaths: 88 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 89 | # options: 90 | # delimiter: '/' 91 | # index: 1 92 | # create: true 93 | # - select: 94 | # kind: MutatingWebhookConfiguration 95 | # fieldPaths: 96 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 97 | # options: 98 | # delimiter: '/' 99 | # index: 1 100 | # create: true 101 | # - select: 102 | # kind: CustomResourceDefinition 103 | # fieldPaths: 104 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 105 | # options: 106 | # delimiter: '/' 107 | # index: 1 108 | # create: true 109 | # - source: # Add cert-manager annotation to the webhook Service 110 | # kind: Service 111 | # version: v1 112 | # name: webhook-service 113 | # fieldPath: .metadata.name # namespace of the service 114 | # targets: 115 | # - select: 116 | # kind: Certificate 117 | # group: cert-manager.io 118 | # version: v1 119 | # fieldPaths: 120 | # - .spec.dnsNames.0 121 | # - .spec.dnsNames.1 122 | # options: 123 | # delimiter: '.' 124 | # index: 0 125 | # create: true 126 | # - source: 127 | # kind: Service 128 | # version: v1 129 | # name: webhook-service 130 | # fieldPath: .metadata.namespace # namespace of the service 131 | # targets: 132 | # - select: 133 | # kind: Certificate 134 | # group: cert-manager.io 135 | # version: v1 136 | # fieldPaths: 137 | # - .spec.dnsNames.0 138 | # - .spec.dnsNames.1 139 | # options: 140 | # delimiter: '.' 141 | # index: 1 142 | # create: true 143 | -------------------------------------------------------------------------------- /internal/containerd/restart_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | // +build unix 3 | 4 | /* 5 | Copyright The KWasm Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package containerd 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "log/slog" 26 | "os" 27 | "os/exec" 28 | "regexp" 29 | "syscall" 30 | 31 | "github.com/mitchellh/go-ps" 32 | ) 33 | 34 | var psProcesses = ps.Processes 35 | 36 | type defaultRestarter struct{} 37 | 38 | func NewDefaultRestarter() Restarter { 39 | return defaultRestarter{} 40 | } 41 | 42 | func (c defaultRestarter) Restart() error { 43 | // If listing systemd units succeeds, prefer systemctl restart; otherwise kill pid 44 | if _, err := ListSystemdUnits(); err == nil { 45 | out, err := nsenterCmd("systemctl", "restart", "containerd").CombinedOutput() 46 | slog.Debug(string(out)) 47 | if err != nil { 48 | return fmt.Errorf("unable to restart containerd: %w", err) 49 | } 50 | } else { 51 | pid, err := getPid("containerd") 52 | if err != nil { 53 | return err 54 | } 55 | slog.Debug("found containerd process", "pid", pid) 56 | 57 | err = syscall.Kill(pid, syscall.SIGHUP) 58 | if err != nil { 59 | return fmt.Errorf("failed to send SIGHUP to containerd: %w", err) 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | type K0sRestarter struct{} 67 | 68 | func (c K0sRestarter) Restart() error { 69 | // First, collect systemd units to determine which mode k0s is running in, eg 70 | // k0sworker or k0scontroller 71 | units, err := ListSystemdUnits() 72 | if err != nil { 73 | return fmt.Errorf("unable to list systemd units: %w", err) 74 | } 75 | service := regexp.MustCompile("k0sworker|k0scontroller").FindString(string(units)) 76 | 77 | out, err := nsenterCmd("systemctl", "restart", service).CombinedOutput() 78 | slog.Debug(string(out)) 79 | if err != nil { 80 | return fmt.Errorf("unable to restart %s: %w", service, err) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | type K3sRestarter struct{} 87 | 88 | func (c K3sRestarter) Restart() error { 89 | // This restarter will be used both for stock K3s distros, which use systemd as well as K3d, which does not. 90 | 91 | // If listing systemd units succeeds, prefer systemctl restart; otherwise kill pid 92 | if _, err := ListSystemdUnits(); err == nil { 93 | out, err := nsenterCmd("systemctl", "restart", "k3s").CombinedOutput() 94 | slog.Debug(string(out)) 95 | if err != nil { 96 | return fmt.Errorf("unable to restart k3s: %w", err) 97 | } 98 | } else { 99 | // TODO: this approach still leads to the behavior mentioned in https://github.com/spinframework/runtime-class-manager/issues/140: 100 | // The first pod's provisioner container exits with code 255, leading to pod status Unknown, 101 | // followed by the subsequent pod's provisioner container no-op-ing and finishing with status Completed. 102 | pid, err := getPid("k3s") 103 | if err != nil { 104 | return err 105 | } 106 | slog.Debug("found k3s process", "pid", pid) 107 | 108 | err = syscall.Kill(pid, syscall.SIGHUP) 109 | if err != nil { 110 | return fmt.Errorf("failed to send SIGHUP to k3s: %w", err) 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | type MicroK8sRestarter struct{} 118 | 119 | func (c MicroK8sRestarter) Restart() error { 120 | out, err := nsenterCmd("systemctl", "restart", "snap.microk8s.daemon-containerd").CombinedOutput() 121 | slog.Debug(string(out)) 122 | if err != nil { 123 | return fmt.Errorf("unable to restart snap.microk8s.daemon-containerd: %w", err) 124 | } 125 | 126 | return nil 127 | } 128 | 129 | type RKE2Restarter struct{} 130 | 131 | func (c RKE2Restarter) Restart() error { 132 | // First, collect systemd units to determine which mode rke2 is running in, eg 133 | // rke2-agent or rke2-server 134 | units, err := ListSystemdUnits() 135 | if err != nil { 136 | return fmt.Errorf("unable to list systemd units: %w", err) 137 | } 138 | service := regexp.MustCompile("rke2-agent|rke2-server").FindString(string(units)) 139 | 140 | out, err := nsenterCmd("systemctl", "restart", service).CombinedOutput() 141 | slog.Debug(string(out)) 142 | if err != nil { 143 | return fmt.Errorf("unable to restart %s: %w", service, err) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func ListSystemdUnits() ([]byte, error) { 150 | return nsenterCmd("systemctl", "list-units", "--type", "service").CombinedOutput() 151 | } 152 | 153 | func nsenterCmd(cmd ...string) *exec.Cmd { 154 | return exec.CommandContext(context.Background(), "nsenter", 155 | append([]string{fmt.Sprintf("-m/%s/proc/1/ns/mnt", os.Getenv("HOST_ROOT")), "--"}, cmd...)...) // #nosec G204 156 | } 157 | 158 | func getPid(executable string) (int, error) { 159 | processes, err := psProcesses() 160 | if err != nil { 161 | return 0, fmt.Errorf("could not get processes: %w", err) 162 | } 163 | 164 | var containerdProcesses = []ps.Process{} 165 | 166 | for _, process := range processes { 167 | if process.Executable() == executable { 168 | containerdProcesses = append(containerdProcesses, process) 169 | } 170 | } 171 | 172 | if len(containerdProcesses) != 1 { 173 | return 0, fmt.Errorf("need exactly one %s process, found: %d", executable, len(containerdProcesses)) 174 | } 175 | 176 | return containerdProcesses[0].Pid(), nil 177 | } 178 | -------------------------------------------------------------------------------- /cmd/node-installer/detect_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The SpinKube Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main_test 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/spf13/afero" 24 | main "github.com/spinframework/runtime-class-manager/cmd/node-installer" 25 | "github.com/spinframework/runtime-class-manager/internal/preset" 26 | tests "github.com/spinframework/runtime-class-manager/tests/node-installer" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func Test_DetectDistro(t *testing.T) { 31 | type args struct { 32 | config main.Config 33 | hostFs afero.Fs 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | wantErr bool 39 | wantPreset preset.Settings 40 | }{ 41 | { 42 | "config_override", 43 | args{ 44 | main.Config{ 45 | struct { 46 | Name string 47 | ConfigPath string 48 | Options map[string]string 49 | }{"containerd", preset.MicroK8s.ConfigPath, nil}, 50 | struct { 51 | Path string 52 | AssetPath string 53 | }{"/opt/rcm", "/assets"}, 54 | struct{ RootPath string }{""}, 55 | }, 56 | tests.FixtureFs("../../testdata/node-installer/distros/default"), 57 | }, 58 | false, 59 | preset.MicroK8s, 60 | }, 61 | { 62 | "config_not_found_fallback_default", 63 | args{ 64 | main.Config{ 65 | struct { 66 | Name string 67 | ConfigPath string 68 | Options map[string]string 69 | }{"containerd", "/etc/containerd/not_found.toml", nil}, 70 | struct { 71 | Path string 72 | AssetPath string 73 | }{"/opt/rcm", "/assets"}, 74 | struct{ RootPath string }{""}, 75 | }, 76 | tests.FixtureFs("../../testdata/node-installer/distros/default"), 77 | }, 78 | false, 79 | preset.Default.WithConfigPath("/etc/containerd/not_found.toml"), 80 | }, 81 | { 82 | "default_and_distro_specific_exist", 83 | args{ 84 | main.Config{ 85 | struct { 86 | Name string 87 | ConfigPath string 88 | Options map[string]string 89 | }{"containerd", "", nil}, 90 | struct { 91 | Path string 92 | AssetPath string 93 | }{"/opt/rcm", "/assets"}, 94 | struct{ RootPath string }{""}, 95 | }, 96 | tests.FixtureFs("../../testdata/node-installer/containerd/default-and-k0s-configs"), 97 | }, 98 | false, 99 | preset.K0s, 100 | }, 101 | { 102 | "unsupported", 103 | args{ 104 | main.Config{ 105 | struct { 106 | Name string 107 | ConfigPath string 108 | Options map[string]string 109 | }{"containerd", "", nil}, 110 | struct { 111 | Path string 112 | AssetPath string 113 | }{"/opt/rcm", "/assets"}, 114 | struct{ RootPath string }{""}, 115 | }, 116 | tests.FixtureFs("../../testdata/node-installer/distros/unsupported"), 117 | }, 118 | true, 119 | preset.Default, 120 | }, 121 | { 122 | "microk8s", 123 | args{ 124 | main.Config{ 125 | struct { 126 | Name string 127 | ConfigPath string 128 | Options map[string]string 129 | }{"containerd", "", nil}, 130 | struct { 131 | Path string 132 | AssetPath string 133 | }{"/opt/rcm", "/assets"}, 134 | struct{ RootPath string }{""}, 135 | }, 136 | tests.FixtureFs("../../testdata/node-installer/distros/microk8s"), 137 | }, 138 | false, 139 | preset.MicroK8s, 140 | }, 141 | { 142 | "k0s", 143 | args{ 144 | main.Config{ 145 | struct { 146 | Name string 147 | ConfigPath string 148 | Options map[string]string 149 | }{"containerd", "", nil}, 150 | struct { 151 | Path string 152 | AssetPath string 153 | }{"/opt/rcm", "/assets"}, 154 | struct{ RootPath string }{""}, 155 | }, 156 | tests.FixtureFs("../../testdata/node-installer/distros/k0s"), 157 | }, 158 | false, 159 | preset.K0s, 160 | }, 161 | { 162 | "k3s", 163 | args{ 164 | main.Config{ 165 | struct { 166 | Name string 167 | ConfigPath string 168 | Options map[string]string 169 | }{"containerd", "", nil}, 170 | struct { 171 | Path string 172 | AssetPath string 173 | }{"/opt/rcm", "/assets"}, 174 | struct{ RootPath string }{""}, 175 | }, 176 | tests.FixtureFs("../../testdata/node-installer/distros/k3s"), 177 | }, 178 | false, 179 | preset.K3s, 180 | }, 181 | { 182 | "rke2", 183 | args{ 184 | main.Config{ 185 | struct { 186 | Name string 187 | ConfigPath string 188 | Options map[string]string 189 | }{"containerd", "", nil}, 190 | struct { 191 | Path string 192 | AssetPath string 193 | }{"/opt/rcm", "/assets"}, 194 | struct{ RootPath string }{""}, 195 | }, 196 | tests.FixtureFs("../../testdata/node-installer/distros/rke2"), 197 | }, 198 | false, 199 | preset.RKE2, 200 | }, 201 | } 202 | for _, tt := range tests { 203 | t.Run(tt.name, func(t *testing.T) { 204 | preset, err := main.DetectDistro(tt.args.config, tt.args.hostFs) 205 | if tt.wantErr { 206 | require.Error(t, err) 207 | } else { 208 | require.NoError(t, err) 209 | require.Equal(t, tt.wantPreset.ConfigPath, preset.ConfigPath) 210 | require.Equal(t, reflect.ValueOf(tt.wantPreset.Setup), reflect.ValueOf(preset.Setup)) 211 | require.Equal(t, reflect.ValueOf(tt.wantPreset.Restarter), reflect.ValueOf(preset.Restarter)) 212 | } 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /internal/controller/job_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/rs/zerolog/log" 24 | batchv1 "k8s.io/api/batch/v1" 25 | corev1 "k8s.io/api/core/v1" 26 | apierrors "k8s.io/apimachinery/pkg/api/errors" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/types" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | ) 32 | 33 | // JobReconciler reconciles a Job object 34 | type JobReconciler struct { 35 | client.Client 36 | Scheme *runtime.Scheme 37 | } 38 | 39 | //+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete 40 | //+kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get;update;patch 41 | //+kubebuilder:rbac:groups=batch,resources=jobs/finalizers,verbs=update 42 | 43 | // SetupWithManager sets up the controller with the Manager. 44 | func (jr *JobReconciler) SetupWithManager(mgr ctrl.Manager) error { 45 | return ctrl.NewControllerManagedBy(mgr). 46 | // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument 47 | For(&batchv1.Job{}). 48 | Complete(jr) 49 | } 50 | 51 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 52 | // move the current state of the cluster closer to the desired state. 53 | // TODO(user): Modify the Reconcile function to compare the state specified by 54 | // the Job object against the actual cluster state, and then 55 | // perform operations to make the cluster state reflect the state specified by 56 | // the user. 57 | // 58 | // For more details, check Reconcile and its Result here: 59 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile 60 | func (jr *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 61 | log := log.With().Str("job", req.Name).Logger() 62 | log.Debug().Msg("Job Reconciliation started!") 63 | 64 | job := &batchv1.Job{} 65 | 66 | if err := jr.Get(ctx, req.NamespacedName, job); err != nil { 67 | if apierrors.IsNotFound(err) { 68 | // we'll ignore not-found errors, since they can't be fixed by an immediate 69 | // requeue (we'll need to wait for a new notification), and we can get them 70 | // on deleted requests. 71 | return ctrl.Result{}, nil 72 | } 73 | log.Error().Msgf("Unable to get Job: %s", err) 74 | return ctrl.Result{}, fmt.Errorf("failed to get Job: %w", err) 75 | } 76 | 77 | if _, exists := job.Labels["spinkube.dev/shimName"]; !exists { 78 | return ctrl.Result{}, nil 79 | } 80 | 81 | shimName := job.Labels["spinkube.dev/shimName"] 82 | 83 | node, err := jr.getNode(ctx, job.Spec.Template.Spec.NodeName) 84 | if err != nil { 85 | return ctrl.Result{}, err 86 | } 87 | 88 | _, finishedType := jr.isJobFinished(job) 89 | switch finishedType { 90 | case "": // ongoing 91 | log.Info().Msgf("Job %s is still Ongoing", job.Name) 92 | return ctrl.Result{}, nil 93 | case batchv1.JobFailed: 94 | log.Info().Msgf("Job %s is still failing...", job.Name) 95 | if err := jr.updateNodeLabels(ctx, node, shimName, "failed"); err != nil { 96 | log.Error().Msgf("Unable to update node label %s: %s", shimName, err) 97 | } 98 | return ctrl.Result{}, nil 99 | case batchv1.JobFailureTarget: 100 | log.Info().Msgf("Job %s is about to fail", job.Name) 101 | if err := jr.updateNodeLabels(ctx, node, shimName, "failed"); err != nil { 102 | log.Error().Msgf("Unable to update node label %s: %s", shimName, err) 103 | } 104 | return ctrl.Result{}, nil 105 | case batchv1.JobComplete: 106 | log.Info().Msgf("Job %s is Completed.", job.Name) 107 | 108 | installOrUninstall := job.Annotations["spinkube.dev/operation"] 109 | 110 | switch installOrUninstall { 111 | case INSTALL: 112 | if err := jr.updateNodeLabels(ctx, node, shimName, "provisioned"); err != nil { 113 | log.Error().Msgf("Unable to update node label %s: %s", shimName, err) 114 | } 115 | case UNINSTALL: 116 | if err := jr.deleteNodeLabel(ctx, node, shimName); err != nil { 117 | log.Error().Msgf("Unable to delete node label %s: %s", shimName, err) 118 | } 119 | } 120 | 121 | return ctrl.Result{}, err 122 | case batchv1.JobSuspended: 123 | log.Info().Msgf("Job %s is suspended", job.Name) 124 | return ctrl.Result{}, nil 125 | } 126 | 127 | return ctrl.Result{}, nil 128 | } 129 | 130 | func (jr *JobReconciler) updateNodeLabels(ctx context.Context, node *corev1.Node, shimName string, status string) error { 131 | node.Labels[shimName] = status 132 | 133 | if err := jr.Update(ctx, node); err != nil { 134 | return fmt.Errorf("failed to update node labels: %w", err) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (jr *JobReconciler) deleteNodeLabel(ctx context.Context, node *corev1.Node, shimName string) error { 141 | delete(node.Labels, shimName) 142 | 143 | if err := jr.Update(ctx, node); err != nil { 144 | return fmt.Errorf("failed to delete node labels: %w", err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (jr *JobReconciler) getNode(ctx context.Context, nodeName string) (*corev1.Node, error) { 151 | node := corev1.Node{} 152 | if err := jr.Get(ctx, types.NamespacedName{Name: nodeName}, &node); err != nil { 153 | log.Err(err).Msg("Unable to fetch node") 154 | return &corev1.Node{}, client.IgnoreNotFound(err) 155 | } 156 | return &node, nil 157 | } 158 | 159 | func (jr *JobReconciler) isJobFinished(job *batchv1.Job) (bool, batchv1.JobConditionType) { 160 | for _, c := range job.Status.Conditions { 161 | if (c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed) && c.Status == corev1.ConditionTrue { 162 | return true, c.Type 163 | } 164 | } 165 | 166 | return false, "" 167 | } 168 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: runtime-class-manager release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | ci: 12 | uses: ./.github/workflows/ci.yml 13 | permissions: 14 | contents: read 15 | pull-requests: read 16 | checks: write 17 | 18 | build-manager: 19 | name: Build manager image, sign it, and generate SBOMs 20 | uses: ./.github/workflows/manager-build.yml 21 | permissions: 22 | id-token: write 23 | packages: write 24 | contents: read 25 | 26 | build-installer: 27 | name: Build installer image, sign it, and generate SBOMs 28 | uses: ./.github/workflows/installer-build.yml 29 | permissions: 30 | id-token: write 31 | packages: write 32 | contents: read 33 | 34 | build-downloader: 35 | name: Build downloader image, sign it, and generate SBOMs 36 | uses: ./.github/workflows/downloader-build.yml 37 | permissions: 38 | id-token: write 39 | packages: write 40 | contents: read 41 | 42 | publish-chart: 43 | name: Publish the helm chart to the configured OCI registry 44 | uses: ./.github/workflows/helm-chart-release.yml 45 | permissions: 46 | packages: write 47 | contents: read 48 | needs: 49 | - ci 50 | - build-manager 51 | - build-installer 52 | - build-downloader 53 | 54 | release: 55 | name: Create release 56 | 57 | needs: 58 | - ci 59 | - build-manager 60 | - build-installer 61 | - build-downloader 62 | - publish-chart 63 | 64 | permissions: 65 | contents: write 66 | 67 | runs-on: ubuntu-latest 68 | 69 | steps: 70 | - name: Retrieve tag name 71 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 72 | run: | 73 | echo TAG_NAME=$(echo ${{ github.ref_name }}) >> $GITHUB_ENV 74 | 75 | - name: Get release ID from the release created by release drafter 76 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 77 | with: 78 | script: | 79 | let releases = await github.rest.repos.listReleases({ 80 | owner: context.repo.owner, 81 | repo: context.repo.repo, 82 | }); 83 | for (const release of releases.data) { 84 | if (release.draft) { 85 | core.info(release) 86 | core.exportVariable('RELEASE_ID', release.id) 87 | return 88 | } 89 | } 90 | core.setFailed(`Draft release not found`) 91 | 92 | - name: Download SBOM artifact 93 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 94 | with: 95 | pattern: "*-sbom-*" 96 | path: ./ 97 | merge-multiple: true 98 | 99 | - name: Download helm chart artifact 100 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 101 | with: 102 | name: runtime-class-manager 103 | path: ./ 104 | 105 | - name: Display structure of downloaded files 106 | run: ls -R 107 | 108 | - name: Upload release assets 109 | id: upload_release_assets 110 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 111 | with: 112 | script: | 113 | let fs = require('fs'); 114 | let path = require('path'); 115 | 116 | // The chart version omits the leading 'v' to adhere to Helm's versioning requirements 117 | let chartVersion = "${{ env.TAG_NAME }}".replace("v", ""); 118 | 119 | let files = [ 120 | 'runtime-class-manager-sbom-amd64.spdx', 121 | 'runtime-class-manager-sbom-amd64.spdx.cert', 122 | 'runtime-class-manager-sbom-amd64.spdx.sig', 123 | 'runtime-class-manager-sbom-arm64.spdx', 124 | 'runtime-class-manager-sbom-arm64.spdx.cert', 125 | 'runtime-class-manager-sbom-arm64.spdx.sig', 126 | 'node-installer-sbom-amd64.spdx', 127 | 'node-installer-sbom-amd64.spdx.cert', 128 | 'node-installer-sbom-amd64.spdx.sig', 129 | 'node-installer-sbom-arm64.spdx', 130 | 'node-installer-sbom-arm64.spdx.cert', 131 | 'node-installer-sbom-arm64.spdx.sig', 132 | 'shim-downloader-sbom-amd64.spdx', 133 | 'shim-downloader-sbom-amd64.spdx.cert', 134 | 'shim-downloader-sbom-amd64.spdx.sig', 135 | 'shim-downloader-sbom-arm64.spdx', 136 | 'shim-downloader-sbom-arm64.spdx.cert', 137 | 'shim-downloader-sbom-arm64.spdx.sig', 138 | `runtime-class-manager-${chartVersion}.tgz`, 139 | ] 140 | const {RELEASE_ID} = process.env 141 | 142 | for (const file of files) { 143 | let file_data = fs.readFileSync(file); 144 | 145 | let response = await github.rest.repos.uploadReleaseAsset({ 146 | owner: context.repo.owner, 147 | repo: context.repo.repo, 148 | release_id: `${RELEASE_ID}`, 149 | name: path.basename(file), 150 | data: file_data, 151 | }); 152 | } 153 | 154 | - name: Publish release 155 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 156 | with: 157 | script: | 158 | const {RELEASE_ID} = process.env 159 | const {TAG_NAME} = process.env 160 | github.rest.repos.updateRelease({ 161 | owner: context.repo.owner, 162 | repo: context.repo.repo, 163 | release_id: `${RELEASE_ID}`, 164 | draft: false, 165 | tag_name: `${TAG_NAME}`, 166 | name: `${TAG_NAME}`, 167 | prerelease: `${{ contains(github.event.workflow_run.head_branch, '-alpha') || contains(github.event.workflow_run.head_branch, '-beta') || contains(github.event.workflow_run.head_branch, '-rc') }}`, 168 | make_latest: `${{ !(contains(github.event.workflow_run.head_branch, '-alpha') || contains(github.event.workflow_run.head_branch, '-beta') || contains(github.event.workflow_run.head_branch, '-rc')) }}` 169 | }); 170 | -------------------------------------------------------------------------------- /.github/workflows/helm-chart-node-scaling-test.yml: -------------------------------------------------------------------------------- 1 | name: Helm Node Scaling Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | SHIM_SPIN_VERSION: v0.19.0 11 | DOCKER_BUILD_SUMMARY: false 12 | 13 | jobs: 14 | helm-node-scaling-test: 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | 19 | - name: Install helm 20 | uses: Azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 21 | with: 22 | version: v3.15.4 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 29 | 30 | - name: Build RCM 31 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 32 | with: 33 | context: . 34 | file: ./Dockerfile 35 | platforms: linux/amd64 36 | cache-from: type=gha 37 | cache-to: type=gha,mode=max 38 | load: true 39 | tags: | 40 | runtime-class-manager:chart-test 41 | 42 | - name: Build node installer 43 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 44 | with: 45 | context: . 46 | file: ./images/installer/Dockerfile 47 | platforms: linux/amd64 48 | cache-from: type=gha 49 | cache-to: type=gha,mode=max 50 | load: true 51 | tags: | 52 | node-installer:chart-test 53 | 54 | - name: Build shim downloader 55 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 56 | with: 57 | context: ./images/downloader 58 | file: ./images/downloader/Dockerfile 59 | platforms: linux/amd64 60 | cache-from: type=gha 61 | cache-to: type=gha,mode=max 62 | load: true 63 | tags: | 64 | shim-downloader:chart-test 65 | 66 | - name: create kind config 67 | run: | 68 | cat << EOF > kind-config.yaml 69 | kind: Cluster 70 | apiVersion: kind.x-k8s.io/v1alpha4 71 | nodes: 72 | - role: control-plane 73 | - role: worker 74 | labels: 75 | spin: true 76 | EOF 77 | 78 | - name: fetch kindscaler script 79 | run: | 80 | curl -so kindscaler.sh https://raw.githubusercontent.com/lobuhi/kindscaler/refs/heads/main/kindscaler.sh 81 | chmod +x kindscaler.sh 82 | 83 | - name: create kind cluster 84 | uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0 85 | with: 86 | cluster_name: kind 87 | config: kind-config.yaml 88 | 89 | - name: import images into kind cluster 90 | run: | 91 | kind load docker-image runtime-class-manager:chart-test 92 | kind load docker-image node-installer:chart-test 93 | kind load docker-image shim-downloader:chart-test 94 | 95 | - name: helm install runtime-class-manager 96 | run: | 97 | helm install rcm \ 98 | --namespace rcm \ 99 | --create-namespace \ 100 | --debug \ 101 | --set image.repository=runtime-class-manager \ 102 | --set image.tag=chart-test \ 103 | --set rcm.nodeInstallerImage.repository=node-installer \ 104 | --set rcm.nodeInstallerImage.tag=chart-test \ 105 | --set rcm.shimDownloaderImage.repository=shim-downloader \ 106 | --set rcm.shimDownloaderImage.tag=chart-test \ 107 | deploy/helm 108 | 109 | - name: apply Spin shim 110 | run: | 111 | # Ensure shim binary is compatible with runner arch 112 | yq -i '.spec.fetchStrategy.anonHttp.location = "https://github.com/spinkube/containerd-shim-spin/releases/download/${{ env.SHIM_SPIN_VERSION }}/containerd-shim-spin-v2-linux-x86_64.tar.gz"' \ 113 | config/samples/test_shim_spin.yaml 114 | kubectl apply -f config/samples/test_shim_spin.yaml 115 | 116 | - name: verify shim is installed into one node 117 | run: | 118 | timeout 1m bash -c 'until [[ "$(kubectl get node -l spin=true -l spin-v2=provisioned -o name | wc -l)" == "1" ]]; do sleep 2; done' 119 | timeout 1m bash -c 'until [[ "$(kubectl get shim.runtime.spinkube.dev/spin-v2 -o json | jq -r '.status.nodesReady')" == "1" ]]; do sleep 2; done' 120 | 121 | - name: scale kind worker nodes to 2 122 | run: ./kindscaler.sh kind -r worker -c 1 123 | 124 | - name: re-import images into kind cluster, for new node 125 | run: | 126 | kind load docker-image runtime-class-manager:chart-test 127 | kind load docker-image node-installer:chart-test 128 | kind load docker-image shim-downloader:chart-test 129 | 130 | - name: verify shim is installed into two nodes 131 | run: | 132 | timeout 1m bash -c 'until [[ "$(kubectl get node -l spin=true -l spin-v2=provisioned -o name | wc -l)" == "2" ]]; do sleep 2; done' 133 | timeout 1m bash -c 'until [[ "$(kubectl get shim.runtime.spinkube.dev/spin-v2 -o json | jq -r '.status.nodesReady')" == "2" ]]; do sleep 2; done' 134 | 135 | - name: delete Spin Shim 136 | run: kubectl delete -f config/samples/test_shim_spin.yaml 137 | 138 | - name: verify shim is uninstalled from both nodes 139 | run: | 140 | timeout 1m bash -c 'until [[ "$(kubectl get node -l spin=true -l spin-v2=provisioned -o name | wc -l)" == "0" ]]; do sleep 2; done' 141 | timeout 1m bash -c 'until ! kubectl get shims.runtime.spinkube.dev/spin-v2; do sleep 2; done' 142 | 143 | - name: debug 144 | if: failure() 145 | run: | 146 | kubectl get pods -A 147 | kubectl describe shim spin-v2 148 | kubectl describe runtimeclass wasmtime-spin-v2 149 | kubectl describe -n rcm pod -l job-name=kind-worker-spin-v2-install || true 150 | kubectl logs -n rcm -l app.kubernetes.io/name=runtime-class-manager || true 151 | kubectl describe -n rcm pod -l app.kubernetes.io/name=runtime-class-manager || true 152 | kubectl describe nodes -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2024. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *AnonHTTPSpec) DeepCopyInto(out *AnonHTTPSpec) { 30 | *out = *in 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonHTTPSpec. 34 | func (in *AnonHTTPSpec) DeepCopy() *AnonHTTPSpec { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(AnonHTTPSpec) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in *FetchStrategy) DeepCopyInto(out *FetchStrategy) { 45 | *out = *in 46 | out.AnonHTTP = in.AnonHTTP 47 | } 48 | 49 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FetchStrategy. 50 | func (in *FetchStrategy) DeepCopy() *FetchStrategy { 51 | if in == nil { 52 | return nil 53 | } 54 | out := new(FetchStrategy) 55 | in.DeepCopyInto(out) 56 | return out 57 | } 58 | 59 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 60 | func (in *RollingSpec) DeepCopyInto(out *RollingSpec) { 61 | *out = *in 62 | } 63 | 64 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RollingSpec. 65 | func (in *RollingSpec) DeepCopy() *RollingSpec { 66 | if in == nil { 67 | return nil 68 | } 69 | out := new(RollingSpec) 70 | in.DeepCopyInto(out) 71 | return out 72 | } 73 | 74 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 75 | func (in *RolloutStrategy) DeepCopyInto(out *RolloutStrategy) { 76 | *out = *in 77 | out.Rolling = in.Rolling 78 | } 79 | 80 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStrategy. 81 | func (in *RolloutStrategy) DeepCopy() *RolloutStrategy { 82 | if in == nil { 83 | return nil 84 | } 85 | out := new(RolloutStrategy) 86 | in.DeepCopyInto(out) 87 | return out 88 | } 89 | 90 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 91 | func (in *RuntimeClassSpec) DeepCopyInto(out *RuntimeClassSpec) { 92 | *out = *in 93 | } 94 | 95 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeClassSpec. 96 | func (in *RuntimeClassSpec) DeepCopy() *RuntimeClassSpec { 97 | if in == nil { 98 | return nil 99 | } 100 | out := new(RuntimeClassSpec) 101 | in.DeepCopyInto(out) 102 | return out 103 | } 104 | 105 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 106 | func (in *Shim) DeepCopyInto(out *Shim) { 107 | *out = *in 108 | out.TypeMeta = in.TypeMeta 109 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 110 | in.Spec.DeepCopyInto(&out.Spec) 111 | in.Status.DeepCopyInto(&out.Status) 112 | } 113 | 114 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Shim. 115 | func (in *Shim) DeepCopy() *Shim { 116 | if in == nil { 117 | return nil 118 | } 119 | out := new(Shim) 120 | in.DeepCopyInto(out) 121 | return out 122 | } 123 | 124 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 125 | func (in *Shim) DeepCopyObject() runtime.Object { 126 | if c := in.DeepCopy(); c != nil { 127 | return c 128 | } 129 | return nil 130 | } 131 | 132 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 133 | func (in *ShimList) DeepCopyInto(out *ShimList) { 134 | *out = *in 135 | out.TypeMeta = in.TypeMeta 136 | in.ListMeta.DeepCopyInto(&out.ListMeta) 137 | if in.Items != nil { 138 | in, out := &in.Items, &out.Items 139 | *out = make([]Shim, len(*in)) 140 | for i := range *in { 141 | (*in)[i].DeepCopyInto(&(*out)[i]) 142 | } 143 | } 144 | } 145 | 146 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShimList. 147 | func (in *ShimList) DeepCopy() *ShimList { 148 | if in == nil { 149 | return nil 150 | } 151 | out := new(ShimList) 152 | in.DeepCopyInto(out) 153 | return out 154 | } 155 | 156 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 157 | func (in *ShimList) DeepCopyObject() runtime.Object { 158 | if c := in.DeepCopy(); c != nil { 159 | return c 160 | } 161 | return nil 162 | } 163 | 164 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 165 | func (in *ShimSpec) DeepCopyInto(out *ShimSpec) { 166 | *out = *in 167 | if in.NodeSelector != nil { 168 | in, out := &in.NodeSelector, &out.NodeSelector 169 | *out = make(map[string]string, len(*in)) 170 | for key, val := range *in { 171 | (*out)[key] = val 172 | } 173 | } 174 | out.FetchStrategy = in.FetchStrategy 175 | out.RuntimeClass = in.RuntimeClass 176 | out.RolloutStrategy = in.RolloutStrategy 177 | if in.ContainerdRuntimeOptions != nil { 178 | in, out := &in.ContainerdRuntimeOptions, &out.ContainerdRuntimeOptions 179 | *out = make(map[string]string, len(*in)) 180 | for key, val := range *in { 181 | (*out)[key] = val 182 | } 183 | } 184 | } 185 | 186 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShimSpec. 187 | func (in *ShimSpec) DeepCopy() *ShimSpec { 188 | if in == nil { 189 | return nil 190 | } 191 | out := new(ShimSpec) 192 | in.DeepCopyInto(out) 193 | return out 194 | } 195 | 196 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 197 | func (in *ShimStatus) DeepCopyInto(out *ShimStatus) { 198 | *out = *in 199 | if in.Conditions != nil { 200 | in, out := &in.Conditions, &out.Conditions 201 | *out = make([]v1.Condition, len(*in)) 202 | for i := range *in { 203 | (*in)[i].DeepCopyInto(&(*out)[i]) 204 | } 205 | } 206 | } 207 | 208 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShimStatus. 209 | func (in *ShimStatus) DeepCopy() *ShimStatus { 210 | if in == nil { 211 | return nil 212 | } 213 | out := new(ShimStatus) 214 | in.DeepCopyInto(out) 215 | return out 216 | } 217 | --------------------------------------------------------------------------------