diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index c2d3773..b358e7a 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -70,13 +70,13 @@ jobs: - name: "Update Deployment Image Tag" # [3] working-directory: "deploy" run: | - # Get any tags that were pushed + # Get any tags that were pushed # XXX: Get sha or tag one # Hint: tags are actually full image names with tags here image_tag=$(echo ${{ steps.meta.outputs.tags }} | cut -d',' -f1) - + kustomize edit set image node-specific-sizing=$image_tag - + kustomize build . > deploy.yaml - name: "Upload Deployment YAML" # [4] diff --git a/.github/workflows/publish_charts.yaml b/.github/workflows/publish_charts.yaml new file mode 100644 index 0000000..0336665 --- /dev/null +++ b/.github/workflows/publish_charts.yaml @@ -0,0 +1,24 @@ +name: Publish Helm charts + +permissions: {} + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + packages: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Publish Helm charts + uses: stefanprodan/helm-gh-pages@master + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index a16a924..1bdff3e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,24 @@ Helps you resize pods created by a DaemonSet depending on the amount of allocatable resources present on the node. +## Helm + +### Install + +```bash +helm repo add node-specific-sizing https://manomanotech.github.io/kubernetes-node-specific-sizing/ +helm repo update +helm repo list +helm install node-specific-sizing node-specific-sizing/node-specific-sizing --namespace node-specific-sizing --create-namespace +helm list --namespace node-specific-sizing +``` + +### Uninstall + +```bash +helm uninstall node-specific-sizing --namespace node-specific-sizing +``` + ## How to use 1. Add the `node-specific-sizing.manomano.tech/enabled: "true"` label any pod you'd like to size depending on the node. @@ -19,7 +37,7 @@ Helps you resize pods created by a DaemonSet depending on the amount of allocata - `node-specific-sizing.manomano.tech/maximum-cpu: 4` - `node-specific-sizing.manomano.tech/minimum-memory: 50M` - `node-specific-sizing.manomano.tech/maximum-memory: 4G` - - NOTE: Minimums and maximums are applied to both resource and limits. + - NOTE: Minimums and maximums are applied to both resource and limits. We don't see the need to add different minimums for requests in limits in practice. You may challenge that choice by opening an issue. - NOTE: Minimums and maximums are to be understood per-pod and not per-container. See resource-sizing algorithm for details. @@ -29,7 +47,7 @@ Helps you resize pods created by a DaemonSet depending on the amount of allocata 5. Take care of the following - In some instances, if limit ends up being below request it will be adjusted to be equal to the request. - - WARNING: We have not tested all cases of partial configuration or weird mish-mashes. + - WARNING: We have not tested all cases of partial configuration or weird mish-mashes. - You're safer defining both requests and limits, or just requests if the underlying DaemonSet does not have limits. - Having some containers define a request or limit while others do not is unsupported. @@ -42,7 +60,7 @@ To achieve this, the updated container requests and limits (from here on out, "t follows: - For each container in the pod, and for each tunable, compute the tunable's relative value per container. - For any given container, `relative_tunable = container_tunable / (sum(container_tunables) - sum(excluded_container_tunables))` + For any given container, `relative_tunable = container_tunable / (sum(container_tunables) - sum(excluded_container_tunables))` - Derive a `pod_tunable_budget = allocatable_tunable_on_node * configured_pod_proportion - sum(excluded_container_tunables)`. This represents the resources that will be given to the pod. - Clamp `pod_tunable_budget` if minimums and/or maximums are set for that tunable. - Finally, `new_absolute_tunable = pod_tunable_budget * relative_tunable` spreads the budget between containers. @@ -82,7 +100,7 @@ PC3| .50 .55 .50 .55 // Output: relative_tunables 1. `make build` and `make docker-build` 2. `make deploy` to setup manifests in current context 3. `bin/playground.sh` to setup a K3D playground cluster with a toy daemonset with annotations set -4. `bin/dev_toggle.sh` to reconfigure the K3D playground cluster so that it can reach the webhook server on your workstation, +4. `bin/dev_toggle.sh` to reconfigure the K3D playground cluster so that it can reach the webhook server on your workstation, as well as extracting certs from the cluster. This allows you to use the IDE of your choice and try things directly. -### +### diff --git a/charts/node-specific-sizing/.helmignore b/charts/node-specific-sizing/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/node-specific-sizing/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/node-specific-sizing/Chart.yaml b/charts/node-specific-sizing/Chart.yaml new file mode 100644 index 0000000..a291a0d --- /dev/null +++ b/charts/node-specific-sizing/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: node-specific-sizing +description: A Kubernetes controller that helps resize pods created by a DaemonSet depending on the amount of allocatable resources present on the node. +type: application +version: 0.1.0 +appVersion: "0.1.0" + +dependencies: + - name: cert-manager + repository: https://charts.jetstack.io + condition: certmanager.enabled + alias: certmanager + version: "v1.15.3" diff --git a/charts/node-specific-sizing/templates/_helpers.tpl b/charts/node-specific-sizing/templates/_helpers.tpl new file mode 100644 index 0000000..b84cf39 --- /dev/null +++ b/charts/node-specific-sizing/templates/_helpers.tpl @@ -0,0 +1,76 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "chart.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 "chart.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 "chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + + +{{/* +Common labels +*/}} +{{- define "chart.labels" -}} +helm.sh/chart: {{ include "chart.chart" . }} +{{ include "chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: "node-specific-sizing" +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Allow the release namespace to be overridden for multi-namespace deployments in combined charts +*/}} +{{- define "chart.namespace" -}} +{{- if .Values.namespaceOverride }} +{{- .Values.namespaceOverride }} +{{- else }} +{{- .Release.Namespace }} +{{- end }} +{{- end }} diff --git a/charts/node-specific-sizing/templates/clusterrole.yaml b/charts/node-specific-sizing/templates/clusterrole.yaml new file mode 100644 index 0000000..30e9137 --- /dev/null +++ b/charts/node-specific-sizing/templates/clusterrole.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "chart.fullname" . }}-role + labels: + app.kubernetes.io/component: rbac + {{- include "chart.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch \ No newline at end of file diff --git a/charts/node-specific-sizing/templates/clusterrolebinding.yaml b/charts/node-specific-sizing/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..1c0d357 --- /dev/null +++ b/charts/node-specific-sizing/templates/clusterrolebinding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "chart.fullname" . }}-rolebinding + labels: + app.kubernetes.io/component: rbac + {{- include "chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "chart.name" . }}-role +subjects: +- kind: ServiceAccount + name: {{ include "chart.name" . }} + namespace: {{ include "chart.namespace" . }} \ No newline at end of file diff --git a/charts/node-specific-sizing/templates/deployment.yaml b/charts/node-specific-sizing/templates/deployment.yaml new file mode 100644 index 0000000..677e818 --- /dev/null +++ b/charts/node-specific-sizing/templates/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "chart.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/component: controller + {{- include "chart.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app.kubernetes.io/component: controller + {{- include "chart.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app.kubernetes.io/component: controller + {{- include "chart.selectorLabels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "chart.serviceAccountName" . }} + terminationGracePeriodSeconds: 10 + containers: + - name: {{ .Chart.Name }} + image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} + resources: {{- toYaml .Values.resources | nindent 12 }} + imagePullPolicy: {{ default "IfNotPresent" .Values.image.pullPolicy }} + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: serving-certs + readOnly: true + securityContext: + runAsNonRoot: true + volumes: + - name: serving-certs + secret: + defaultMode: 420 + secretName: {{ include "chart.fullname" . }}-server-cert diff --git a/charts/node-specific-sizing/templates/mutatingadmissionwebhook.yaml b/charts/node-specific-sizing/templates/mutatingadmissionwebhook.yaml new file mode 100644 index 0000000..0dcb5fe --- /dev/null +++ b/charts/node-specific-sizing/templates/mutatingadmissionwebhook.yaml @@ -0,0 +1,29 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "chart.fullname" . }}-webhook + annotations: + cert-manager.io/inject-ca-from: '{{ .Release.Namespace }}/{{ include "chart.fullname" . }}-serving-cert' + labels: + {{- include "chart.labels" . | nindent 4 }} +webhooks: + - name: {{ include "chart.fullname" . }}.svc.cluster.local + objectSelector: + matchLabels: + node-specific-sizing.manomano.tech/enabled: "true" + admissionReviewVersions: [ "v1" ] + sideEffects: None + failurePolicy: Ignore + timeoutSeconds: 1 + clientConfig: + service: + namespace: node-specific-sizing + name: {{ include "chart.fullname" . }} + path: /mutate + rules: + - apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + operations: ["CREATE"] + scope: Namespaced + diff --git a/charts/node-specific-sizing/templates/selfsigned-issuer.yaml b/charts/node-specific-sizing/templates/selfsigned-issuer.yaml new file mode 100644 index 0000000..242c51f --- /dev/null +++ b/charts/node-specific-sizing/templates/selfsigned-issuer.yaml @@ -0,0 +1,16 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "chart.fullname" . }}-selfsigned-issuer + namespace: {{ .Release.Namespace }} + annotations: + {{- if .Values.certmanager.enabled }} + helm.sh/hook: post-install,post-upgrade + {{- else }} + helm.sh/hook: pre-install,pre-upgrade + {{- end }} + helm.sh/hook-weight: "1" + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + selfSigned: {} diff --git a/charts/node-specific-sizing/templates/service.yaml b/charts/node-specific-sizing/templates/service.yaml new file mode 100644 index 0000000..016f7c5 --- /dev/null +++ b/charts/node-specific-sizing/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "chart.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/component: webhook + {{- include "chart.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "chart.selectorLabels" . | nindent 4 }} + ports: + - name: "https" + port: 443 + targetPort: 8443 + protocol: TCP diff --git a/charts/node-specific-sizing/templates/serviceaccount.yaml b/charts/node-specific-sizing/templates/serviceaccount.yaml new file mode 100644 index 0000000..c3c8ac2 --- /dev/null +++ b/charts/node-specific-sizing/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "chart.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/component: rbac + {{- include "chart.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} diff --git a/charts/node-specific-sizing/templates/serving-cert.yaml b/charts/node-specific-sizing/templates/serving-cert.yaml new file mode 100644 index 0000000..8e048af --- /dev/null +++ b/charts/node-specific-sizing/templates/serving-cert.yaml @@ -0,0 +1,22 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "chart.fullname" . }}-serving-cert + namespace: {{ .Release.Namespace }} + annotations: + {{- if .Values.certmanager.enabled }} + helm.sh/hook: post-install,post-upgrade + {{- else }} + helm.sh/hook: pre-install,pre-upgrade + {{- end }} + helm.sh/hook-weight: "2" + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + dnsNames: + - '{{ include "chart.fullname" . }}.{{ .Release.Namespace }}.svc' + - '{{ include "chart.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.kubernetesClusterDomain }}' + issuerRef: + kind: Issuer + name: {{ include "chart.fullname" . }}-selfsigned-issuer + secretName: {{ include "chart.fullname" . }}-server-cert diff --git a/charts/node-specific-sizing/values.yaml b/charts/node-specific-sizing/values.yaml new file mode 100644 index 0000000..351683f --- /dev/null +++ b/charts/node-specific-sizing/values.yaml @@ -0,0 +1,28 @@ + +image: + repository: "ghcr.io/manomanotech/kubernetes-node-specific-sizing" + tag: "sha-28e2a98" + +replicas: 3 + +imagePullPolicy: IfNotPresent + +serviceAccount: + create: true + annotations: {} + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + + +# Certmanager specific values +certmanager: + enabled: false + crds: + enabled: true + keep: true \ No newline at end of file