diff --git a/.github/workflows/helm-chart-release.yml b/.github/workflows/helm-chart-release.yml index 6233414e..5e1cc555 100644 --- a/.github/workflows/helm-chart-release.yml +++ b/.github/workflows/helm-chart-release.yml @@ -1,4 +1,4 @@ -# This action releases the kwasm-operator helm chart +# This action releases the runtime-class-manager helm chart # The action must run on each commit done against main, however # a new release will be performed **only** when a change occurs inside # of the `charts` directory. @@ -40,6 +40,8 @@ jobs: - name: Run chart-releaser if: github.ref == 'refs/heads/main' uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 + with: + charts_dir: deploy/helm env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_RELEASE_NAME_TEMPLATE: "{{ .Name }}-chart-{{ .Version }}" diff --git a/Tiltfile b/Tiltfile index 1017b762..4f90e8dd 100644 --- a/Tiltfile +++ b/Tiltfile @@ -9,16 +9,16 @@ kubectl_cmd = "kubectl" if str(local("command -v " + kubectl_cmd + " || true", quiet = True)) == "": fail("Required command '" + kubectl_cmd + "' not found in PATH") -# Create the kwasm namespace +# Create the rcm namespace # This is required since the helm() function doesn't support the create_namespace flag load('ext://namespace', 'namespace_create') -namespace_create('kwasm') +namespace_create('rcm') -# Install kwasm-operator helm chart +# Install runtime-class-manager helm chart install = helm( - './charts/kwasm-operator/', - name='kwasm-operator', - namespace='kwasm', + './deploy/helm', + name='runtime-class-manager', + namespace='rcm', set=['image.repository=' + settings.get('registry')] ) @@ -26,7 +26,7 @@ objects = decode_yaml_stream(install) for o in objects: # Update the root security group. Tilt requires root access to update the # running process. - if o.get('kind') == 'Deployment' and o.get('metadata').get('name') == 'kwasm-operator': + if o.get('kind') == 'Deployment' and o.get('metadata').get('name') == 'runtime-class-manager': o['spec']['template']['spec']['securityContext']['runAsNonRoot'] = False # Disable the leader election to speed up the startup time. o['spec']['template']['spec']['containers'][0]['args'].remove('--leader-elect') diff --git a/config/samples/test_shim_spin.yaml b/config/samples/test_shim_spin.yaml index 82b81180..6a6b7cba 100644 --- a/config/samples/test_shim_spin.yaml +++ b/config/samples/test_shim_spin.yaml @@ -15,11 +15,13 @@ spec: fetchStrategy: type: annonymousHttp anonHttp: - location: "https://github.com/spinkube/containerd-shim-spin/releases/download/v0.14.1/containerd-shim-spin-v2-linux-aarch64.tar.gz" + location: "https://github.com/spinkube/containerd-shim-spin/releases/download/v0.15.1/containerd-shim-spin-v2-linux-aarch64.tar.gz" runtimeClass: - name: spin-v2 - handler: spin + # Note: this name is used by the Spin Operator project as its default: + # https://github.com/spinkube/spin-operator/blob/main/config/samples/spin-shim-executor.yaml + name: wasmtime-spin-v2 + handler: spin-v2 rolloutStrategy: type: recreate diff --git a/deploy/helm/.helmignore b/deploy/helm/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml new file mode 100644 index 00000000..6527c4a0 --- /dev/null +++ b/deploy/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: runtime-class-manager +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/deploy/helm/README.md b/deploy/helm/README.md new file mode 100644 index 00000000..aa90922e --- /dev/null +++ b/deploy/helm/README.md @@ -0,0 +1,40 @@ +# runtime-class-manager + +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.kwasm.sh_shims.yaml). + +## Prerequisites + +- [Kubernetes v1.20+](https://kubernetes.io/docs/setup/) +- [Helm v3](https://helm.sh/docs/intro/install/) + +## Installing the chart + +The following installs the runtime-class-manager chart with the release name `rcm`: + +```shell +helm upgrade --install \ + --namespace rcm \ + --create-namespace \ + --wait \ + rcm . +``` + +## Post-installation + +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/). + +> Note: Ensure that the `location` for the specified shim binary points to the correct architecture for your Node(s) + +For example, here we install the Spin shim: + +```shell +kubectl apply -f ../../config/samples/test_shim_spin.yaml +``` + +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: + +```shell +kubectl label node --all spin=true +``` + +You are now ready to deploy your Wasm workloads. diff --git a/deploy/helm/crds/runtime.kwasm.sh_shims.yaml b/deploy/helm/crds/runtime.kwasm.sh_shims.yaml new file mode 100644 index 00000000..8b8cb649 --- /dev/null +++ b/deploy/helm/crds/runtime.kwasm.sh_shims.yaml @@ -0,0 +1,180 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: shims.runtime.kwasm.sh +spec: + group: runtime.kwasm.sh + names: + kind: Shim + listKind: ShimList + plural: shims + singular: shim + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.runtimeClass.name + name: RuntimeClass + type: string + - jsonPath: .status.nodesReady + name: Ready + type: integer + - jsonPath: .status.nodes + name: Nodes + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: Shim is the Schema for the shims API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ShimSpec defines the desired state of Shim + properties: + fetchStrategy: + properties: + anonHttp: + properties: + location: + type: string + required: + - location + type: object + type: + type: string + required: + - anonHttp + - type + type: object + nodeSelector: + additionalProperties: + type: string + type: object + rolloutStrategy: + properties: + rolling: + properties: + maxUpdate: + type: integer + required: + - maxUpdate + type: object + type: + enum: + - rolling + - recreate + type: string + required: + - type + type: object + runtimeClass: + properties: + handler: + type: string + name: + type: string + required: + - handler + - name + type: object + required: + - fetchStrategy + - rolloutStrategy + - runtimeClass + type: object + status: + description: ShimStatus defines the observed state of Shim + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + nodes: + type: integer + nodesReady: + type: integer + required: + - nodes + - nodesReady + type: object + type: object + served: true + storage: true + subresources: {} diff --git a/deploy/helm/templates/NOTES.txt b/deploy/helm/templates/NOTES.txt new file mode 100644 index 00000000..cb30c337 --- /dev/null +++ b/deploy/helm/templates/NOTES.txt @@ -0,0 +1,21 @@ +Welcome to Runtime-Class-Manager. + +Next steps: + +Create one or more Wasm Shim custom resources. See the samples in https://github.com/spinkube/runtime-class-manager/tree/main/config/samples. + +> Note: Ensure that the `location` for the specified shim binary points to the correct architecture for your Node(s) + +For example, install the Spin shim: + +```shell +kubectl apply -f https://raw.githubusercontent.com/spinkube/runtime-class-manager/refs/heads/main/config/samples/test_shim_spin.yaml +``` + +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: + +```shell +kubectl label node --all spin=true +``` + +You are now ready to deploy your Wasm workloads. \ No newline at end of file diff --git a/deploy/helm/templates/_helpers.tpl b/deploy/helm/templates/_helpers.tpl new file mode 100644 index 00000000..3665e213 --- /dev/null +++ b/deploy/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "rcm.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "rcm.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "rcm.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "rcm.labels" -}} +helm.sh/chart: {{ include "rcm.chart" . }} +{{ include "rcm.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "rcm.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rcm.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "rcm.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "rcm.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/templates/clusterrole.yaml b/deploy/helm/templates/clusterrole.yaml new file mode 100644 index 00000000..2173fa3e --- /dev/null +++ b/deploy/helm/templates/clusterrole.yaml @@ -0,0 +1,51 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: {{ include "rcm.fullname" . }} +rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch + - update + +# TODO: It seems like runtime-class-manger should only need to modify jobs in its own namespace, +# i.e. via a namespaced Role. However, RBAC errors result without these clusterrole permissions. +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch + - create + - delete + - patch + +- apiGroups: + - runtime.kwasm.sh + resources: + - shims + verbs: + - get + - list + - watch + - update + +- apiGroups: + - node.k8s.io + resources: + - runtimeclasses + verbs: + - get + - list + - watch + - create + - delete + - patch \ No newline at end of file diff --git a/deploy/helm/templates/clusterrolebinding.yaml b/deploy/helm/templates/clusterrolebinding.yaml new file mode 100644 index 00000000..2d64af45 --- /dev/null +++ b/deploy/helm/templates/clusterrolebinding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + creationTimestamp: null + name: {{ include "rcm.fullname" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "rcm.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "rcm.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml new file mode 100644 index 00000000..6877fba7 --- /dev/null +++ b/deploy/helm/templates/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "rcm.fullname" . }} + labels: + {{- include "rcm.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "rcm.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "rcm.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "rcm.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CONTROLLER_NAMESPACE + value: {{ .Release.Namespace }} + - name: SHIM_DOWNLOADER_IMAGE + value: "{{ .Values.rcm.shimDownloaderImage.repository }}:{{ .Values.rcm.shimDownloaderImage.tag | default .Chart.AppVersion }}" + - name: SHIM_NODE_INSTALLER_IMAGE + value: "{{ .Values.rcm.nodeInstallerImage.repository }}:{{ .Values.rcm.nodeInstallerImage.tag | default .Chart.AppVersion }}" + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/templates/serviceaccount.yaml b/deploy/helm/templates/serviceaccount.yaml new file mode 100644 index 00000000..7bbdfb37 --- /dev/null +++ b/deploy/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "rcm.serviceAccountName" . }} + labels: + {{- include "rcm.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml new file mode 100644 index 00000000..62b381b7 --- /dev/null +++ b/deploy/helm/values.yaml @@ -0,0 +1,101 @@ +# Default values for rcm. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: "ghcr.io/spinkube/runtime-class-manager" + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +rcm: + shimDownloaderImage: + repository: "ghcr.io/spinkube/shim-downloader" + tag: "latest" + nodeInstallerImage: + repository: "ghcr.io/spinkube/node-installer" + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + httpGet: + path: /healthz + port: 8082 + initialDelaySeconds: 15 + periodSeconds: 20 +readinessProbe: + httpGet: + path: /readyz + port: 8082 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {}