diff --git a/Makefile b/Makefile index 6bbd7faac..cc7e900d7 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ KUBE_RELEASES ?= $(shell echo $(SUPPORTED_KUBE_RELEASES) | cut -d ' ' -f 1-$(NUM # The latest kube release LATEST_KUBE_RELEASE ?= $(shell echo $(SUPPORTED_KUBE_RELEASES) | cut -d ' ' -f 1) -BINARY ?= kwok kwokctl +BINARY ?= kwok kwokctl kwok-operator IMAGE_PREFIX ?= @@ -64,9 +64,11 @@ PRE_RELEASE ?= ifeq ($(STAGING_IMAGE_PREFIX),) KWOK_IMAGE ?= kwok +KWOK_OPERATOR_IMAGE ?= operator CLUSTER_IMAGE ?= cluster else KWOK_IMAGE ?= $(STAGING_IMAGE_PREFIX)/kwok +KWOK_OPERATOR_IMAGE ?= $(STAGING_IMAGE_PREFIX)/kwok-operator CLUSTER_IMAGE ?= $(STAGING_IMAGE_PREFIX)/cluster endif @@ -76,7 +78,7 @@ IMAGE_PLATFORMS ?= linux/amd64 linux/arm64 BINARY_PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 -MANIFESTS ?= kwok kwokctl stage/fast metrics/usage +MANIFESTS ?= kwok kwokctl operator stage/fast metrics/usage BUILDER ?= docker DOCKER_CLI_EXPERIMENTAL ?= enabled @@ -187,6 +189,31 @@ cross-image: --builder=${BUILDER} \ --push=${PUSH} +## operator-image: Build kwok operator image +.PHONY: operator-image +operator-image: + @./images/kwok-operator/build.sh \ + $(addprefix --extra-tag=, $(EXTRA_TAGS)) \ + --image=${KWOK_OPERATOR_IMAGE} \ + --version=${VERSION} \ + --staging-prefix=${STAGING_PREFIX} \ + --dry-run=${DRY_RUN} \ + --builder=${BUILDER} \ + --push=${PUSH} + +## cross-operator-image: Build kwok operator images for all supported platforms +.PHONY: cross-operator-image +cross-operator-image: + @./images/kwok-operator/build.sh \ + $(addprefix --platform=, $(IMAGE_PLATFORMS)) \ + $(addprefix --extra-tag=, $(EXTRA_TAGS)) \ + --image=${KWOK_OPERATOR_IMAGE} \ + --version=${VERSION} \ + --staging-prefix=${STAGING_PREFIX} \ + --dry-run=${DRY_RUN} \ + --builder=${BUILDER} \ + --push=${PUSH} + ## cluster-image: Build cluster image .PHONY: cluster-image cluster-image: diff --git a/charts/operator/.helmignore b/charts/operator/.helmignore new file mode 100644 index 000000000..6b8c0abba --- /dev/null +++ b/charts/operator/.helmignore @@ -0,0 +1 @@ +.helmignore diff --git a/charts/operator/Chart.yaml b/charts/operator/Chart.yaml new file mode 100644 index 000000000..fda41169c --- /dev/null +++ b/charts/operator/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +description: KWOK Operator +type: application +home: https://kwok.sigs.k8s.io +icon: https://github.com/kubernetes-sigs/kwok/raw/main/logo/kwok.png +keywords: +- kubernetes +- kwok +- operator +sources: +- https://github.com/kubernetes-sigs/kwok +name: operator +maintainers: +- name: wzshiming + email: wzshiming@hotmail.com +appVersion: v0.6.0 +version: 0.0.1 diff --git a/charts/operator/README.md b/charts/operator/README.md new file mode 100644 index 000000000..2e136aeff --- /dev/null +++ b/charts/operator/README.md @@ -0,0 +1,49 @@ +# KWOK (Kubernetes WithOut Kubelet) + +[KWOK](https://kwok.sigs.k8s.io/) - Simulates thousands of Nodes and Clusters. + +## Installing the Chart + +Before you can install the chart you will need to add the `kwok` repo to [Helm](https://helm.sh/). + +```shell +helm repo add kwok https://kwok.sigs.k8s.io/charts/ +``` + +After you've installed the repo you can install the chart. + +```shell +helm upgrade --namespace kube-system --install kwok kwok/kwok +``` + +Set up default stage policy (required) +> NOTE: This configures the pod/node emulation behavior, if not it will do nothing. + +```shell +helm upgrade --install kwok kwok/stage-fast +``` + +Set up default metrics usage policy (optional) + +```shell +helm upgrade --install kwok kwok/metrics-usage +``` + +## Configuration + +The following table lists the configurable parameters of the kwok chart and their default values. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| fullnameOverride | string | `"kwok-controller"` | Override the `fullname` of the chart. | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | +| image.repository | string | `"registry.k8s.io/kwok/kwok"` | Image repository. | +| image.tag | string | `""` | Overrides the image tag whose default is {{ .Chart.AppVersion }}. | +| imagePullSecrets | list | `[]` | Image pull secrets. | +| nameOverride | string | `""` | Override the `name` of the chart. | +| nodeSelector | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicas | int | `1` | The replica count for Deployment. | +| resources | object | `{}` | | +| securityContext | object | `{}` | | diff --git a/charts/operator/README.md.gotmpl b/charts/operator/README.md.gotmpl new file mode 100644 index 000000000..4b66504e3 --- /dev/null +++ b/charts/operator/README.md.gotmpl @@ -0,0 +1,9 @@ +# {{ template "chart.description" . }} + +{{ template "extra.usage" . }} + +## Configuration + +The following table lists the configurable parameters of the kwok chart and their default values. + +{{ template "chart.valuesTable" . }} diff --git a/charts/operator/crds b/charts/operator/crds new file mode 120000 index 000000000..8890a961f --- /dev/null +++ b/charts/operator/crds @@ -0,0 +1 @@ +../../kustomize/operator/crd/bases \ No newline at end of file diff --git a/charts/operator/templates/_helpers.tpl b/charts/operator/templates/_helpers.tpl new file mode 100644 index 000000000..f3d96e7bb --- /dev/null +++ b/charts/operator/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "kwok.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 "kwok.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 "kwok.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "kwok.labels" -}} +helm.sh/chart: {{ include "kwok.chart" . }} +{{ include "kwok.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "kwok.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kwok.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "kwok.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "kwok.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/operator/templates/deployment.yaml b/charts/operator/templates/deployment.yaml new file mode 100644 index 000000000..6a747a3e1 --- /dev/null +++ b/charts/operator/templates/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kwok.fullname" . }} + labels: + {{- include "kwok.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + {{- include "kwok.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "kwok.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "kwok.fullname" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Always + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + resources: + {{- toYaml .Values.resources | nindent 10 }} + {{- 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/charts/operator/templates/role.yaml b/charts/operator/templates/role.yaml new file mode 100644 index 000000000..9bf52cbd9 --- /dev/null +++ b/charts/operator/templates/role.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kwok-operator +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments/scale + verbs: + - update +- apiGroups: + - kwok.x-k8s.io + resources: + - controllers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kwok.x-k8s.io + resources: + - controllers/status + verbs: + - patch + - update diff --git a/charts/operator/templates/role_binding.yaml b/charts/operator/templates/role_binding.yaml new file mode 100644 index 000000000..2c3bf117f --- /dev/null +++ b/charts/operator/templates/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kwok-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kwok-operator +subjects: +- kind: ServiceAccount + name: kwok-operator + namespace: {{ .Release.Namespace }} diff --git a/charts/operator/templates/service_account.yaml b/charts/operator/templates/service_account.yaml new file mode 100644 index 000000000..f25ba2695 --- /dev/null +++ b/charts/operator/templates/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kwok-operator + namespace: {{ .Release.Namespace }} diff --git a/charts/operator/values.yaml b/charts/operator/values.yaml new file mode 100644 index 000000000..4c8bccf78 --- /dev/null +++ b/charts/operator/values.yaml @@ -0,0 +1,26 @@ +image: + # -- Image pull policy. + pullPolicy: IfNotPresent + # -- Image repository. + repository: registry.k8s.io/kwok/operator + # -- Overrides the image tag whose default is {{ .Chart.AppVersion }}. + tag: "" + +# -- Image pull secrets. +imagePullSecrets: [] + +# -- Override the `name` of the chart. +nameOverride: "" + +# -- Override the `fullname` of the chart. +fullnameOverride: "kwok-operator" + +podSecurityContext: {} +securityContext: {} + +nodeSelector: {} +resources: {} +affinity: {} + +# -- The replica count for Deployment. +replicas: 1 diff --git a/cmd/kwok-operator/main.go b/cmd/kwok-operator/main.go new file mode 100644 index 000000000..aa764e187 --- /dev/null +++ b/cmd/kwok-operator/main.go @@ -0,0 +1,53 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is the entry point for the kwok binary. +package main + +import ( + "os" + + "github.com/spf13/pflag" + + "sigs.k8s.io/kwok/pkg/config" + "sigs.k8s.io/kwok/pkg/log" + "sigs.k8s.io/kwok/pkg/operator/cmd" + "sigs.k8s.io/kwok/pkg/utils/signals" +) + +func main() { + flagset := pflag.NewFlagSet("global", pflag.ContinueOnError) + flagset.ParseErrorsWhitelist.UnknownFlags = true + flagset.Usage = func() {} + + ctx := signals.SetupSignalContext() + ctx, logger := log.InitFlags(ctx, flagset) + + ctx, err := config.InitFlags(ctx, flagset) + if err != nil { + _, _ = os.Stderr.Write([]byte(flagset.FlagUsages())) + logger.Error("Init config flags", err) + os.Exit(1) + } + + command := cmd.NewCommand(ctx) + command.PersistentFlags().AddFlagSet(flagset) + err = command.ExecuteContext(ctx) + if err != nil { + logger.Error("Execute exit", err) + os.Exit(1) + } +} diff --git a/hack/releases-helm-charts.sh b/hack/releases-helm-charts.sh index 034df3e97..deec0a15c 100755 --- a/hack/releases-helm-charts.sh +++ b/hack/releases-helm-charts.sh @@ -85,5 +85,6 @@ chart_dir="./charts" index_dir="${ROOT_DIR}/site/static/charts" package_and_index "${index_dir}" "${chart_dir}/kwok" "kwok-chart" || : +package_and_index "${index_dir}" "${chart_dir}/operator" "kwok-operator-chart" || : package_and_index "${index_dir}" "${chart_dir}/stage-fast" "kwok-stage-fast-chart" || : package_and_index "${index_dir}" "${chart_dir}/metrics-usage" "kwok-metrics-usage-chart" || : diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 838df3210..33afa3058 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -47,6 +47,7 @@ function gen() { deepcopy-gen \ ./pkg/apis/internalversion/ \ ./pkg/apis/v1alpha1/ \ + ./pkg/apis/operator/v1alpha1/ \ ./pkg/apis/config/v1alpha1/ \ ./pkg/apis/action/v1alpha1/ \ --output-file zz_generated.deepcopy.go \ @@ -54,6 +55,7 @@ function gen() { echo "Generating defaulter" defaulter-gen \ ./pkg/apis/v1alpha1/ \ + ./pkg/apis/operator/v1alpha1/ \ ./pkg/apis/config/v1alpha1/ \ ./pkg/apis/action/v1alpha1/ \ --output-file zz_generated.defaults.go \ @@ -70,6 +72,7 @@ function gen() { --clientset-name versioned \ --input-base "" \ --input sigs.k8s.io/kwok/pkg/apis/v1alpha1 \ + --input sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1 \ --output-pkg sigs.k8s.io/kwok/pkg/client/clientset \ --output-dir ./pkg/client/clientset \ --go-header-file ./hack/boilerplate/boilerplate.generatego.txt \ diff --git a/hack/update-controllergen.sh b/hack/update-controllergen.sh index b556a828a..76f53b745 100755 --- a/hack/update-controllergen.sh +++ b/hack/update-controllergen.sh @@ -36,6 +36,17 @@ function gen() { paths=./pkg/apis/v1alpha1/ \ output:crd:artifacts:config=kustomize/crd/bases \ output:rbac:artifacts:config=kustomize/rbac + + rm -rf \ + "${ROOT_DIR}/kustomize/operator/crd/bases" \ + "${ROOT_DIR}/kustomize/operator/rbac/rbac.yaml" + echo "Generating kwok-operator crd/rbac" + controller-gen \ + rbac:roleName=kwok-operator \ + crd:allowDangerousTypes=true \ + paths=./pkg/apis/operator/v1alpha1/ \ + output:crd:artifacts:config=kustomize/operator/crd/bases \ + output:rbac:artifacts:config=kustomize/operator/rbac } cd "${ROOT_DIR}" && gen diff --git a/hack/update-helm-charts.sh b/hack/update-helm-charts.sh index 3c7479af5..f3669342a 100755 --- a/hack/update-helm-charts.sh +++ b/hack/update-helm-charts.sh @@ -66,6 +66,10 @@ function sync() { sync_object_to_chart kustomize/rbac/role_binding.yaml charts/kwok/templates/role_binding.yaml sync_object_to_chart kustomize/rbac/service_account.yaml charts/kwok/templates/service_account.yaml + sync_object_to_chart kustomize/operator/rbac/role.yaml charts/operator/templates/role.yaml + sync_object_to_chart kustomize/operator/rbac/role_binding.yaml charts/operator/templates/role_binding.yaml + sync_object_to_chart kustomize/operator/rbac/service_account.yaml charts/operator/templates/service_account.yaml + sync_stage_to_chart kustomize/stage/pod/fast/pod-ready.yaml charts/stage-fast/templates/pod-ready.yaml sync_stage_to_chart kustomize/stage/pod/fast/pod-complete.yaml charts/stage-fast/templates/pod-complete.yaml sync_stage_to_chart kustomize/stage/pod/fast/pod-delete.yaml charts/stage-fast/templates/pod-delete.yaml diff --git a/hack/verify-yamllint.sh b/hack/verify-yamllint.sh index 931926945..518f5f668 100755 --- a/hack/verify-yamllint.sh +++ b/hack/verify-yamllint.sh @@ -56,6 +56,7 @@ function check() { -o -path ./demo/node_modules/\* \ -o -path ./site/themes/\* \ -o -path ./charts/kwok/templates/\* \ + -o -path ./charts/operator/templates/\* \ \)) "${COMMAND[@]}" -s -c .yamllint.conf "${findfiles[@]}" diff --git a/images/operator/Dockerfile b/images/operator/Dockerfile new file mode 100644 index 000000000..85ca4a124 --- /dev/null +++ b/images/operator/Dockerfile @@ -0,0 +1,20 @@ +# Copyright 2024 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG BASE_IMAGE=docker.io/library/alpine:3.20 +FROM --platform=$TARGETPLATFORM $BASE_IMAGE AS cache +ARG TARGETPLATFORM +COPY --chmod=0755 bin/$TARGETPLATFORM/kwok-operator /usr/local/bin/ + +ENTRYPOINT ["/usr/local/bin/kwok-operator"] diff --git a/images/operator/build.sh b/images/operator/build.sh new file mode 100755 index 000000000..8fe2f447d --- /dev/null +++ b/images/operator/build.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# Copyright 2024 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +DIR="$(dirname "${BASH_SOURCE[0]}")" +DIR="$(realpath "${DIR}")" +ROOT_DIR="$(realpath "${DIR}/../..")" +DOCKERFILE="$(echo "${DIR}/Dockerfile" | sed "s|^${ROOT_DIR}/|./|")" + +BASE_IMAGE= +DRY_RUN=false +PUSH=false +IMAGES=() +EXTRA_TAGS=() +PLATFORMS=() +VERSION="" +STAGING_PREFIX="" +BUILDER="docker" + +function usage() { + echo "Usage: ${0} [--help] [--version ] [--image ...] [--extra-tag ...] [--staging-prefix ] [--platform ...] [--push] [--dry-run] [--builder ]" + echo " --version is kwok version, is required" + echo " --image is image, is required" + echo " --extra-tag is extra tag" + echo " --staging-prefix is staging prefix for tag" + echo " --platform is multi-platform capable for image" + echo " --push will push image to registry" + echo " --dry-run just show what would be done" + echo " --builder specify image builder, default: ${BUILDER}. available options: docker, nerdctl, podman" + echo " --base-image specify base image, default: ${BASE_IMAGE}" +} + +function args() { + local arg + while [[ $# -gt 0 ]]; do + arg="$1" + case "${arg}" in + --version | --version=*) + [[ "${arg#*=}" != "${arg}" ]] && VERSION="${arg#*=}" || { VERSION="${2}" && shift; } || : + shift + ;; + --image | --image=*) + [[ "${arg#*=}" != "${arg}" ]] && IMAGES+=("${arg#*=}") || { IMAGES+=("${2}") && shift; } || : + shift + ;; + --extra-tag | --extra-tag=*) + [[ "${arg#*=}" != "${arg}" ]] && EXTRA_TAGS+=("${arg#*=}") || { EXTRA_TAGS+=("${2}") && shift; } || : + shift + ;; + --staging-prefix | --staging-prefix=*) + [[ "${arg#*=}" != "${arg}" ]] && STAGING_PREFIX="${arg#*=}" || { STAGING_PREFIX="${2}" && shift; } || : + shift + ;; + --platform | --platform=*) + [[ "${arg#*=}" != "${arg}" ]] && PLATFORMS+=("${arg#*=}") || { PLATFORMS+=("${2}") && shift; } || : + shift + ;; + --push | --push=*) + [[ "${arg#*=}" != "${arg}" ]] && PUSH="${arg#*=}" || PUSH="true" || : + shift + ;; + --dry-run | --dry-run=*) + [[ "${arg#*=}" != "${arg}" ]] && DRY_RUN="${arg#*=}" || DRY_RUN="true" || : + shift + ;; + --builder | --builder=*) + [[ "${arg#*=}" != "${arg}" ]] && BUILDER="${arg#*=}" || { BUILDER="${2}" && shift; } || : + shift + ;; + --base-image | --base-image=*) + [[ "${arg#*=}" != "${arg}" ]] && BASE_IMAGE="${arg#*=}" || { BASE_IMAGE="${2}" && shift; } || : + shift + ;; + --help) + usage + exit 0 + ;; + *) + echo "Unknown argument: ${arg}" + usage + exit 1 + ;; + esac + done + + if [[ "${VERSION}" == "" ]]; then + echo "--version is required" + usage + exit 1 + fi + + if [[ "${#IMAGES}" -eq 0 ]]; then + echo "--image is required" + usage + exit 1 + fi + + if [[ "${#PLATFORMS[*]}" -eq 0 ]]; then + PLATFORMS+=( + linux/amd64 + ) + fi +} + +function dry_run() { + echo "$*" + if [[ "${DRY_RUN}" != "true" ]]; then + eval "$*" + fi +} + +function main() { + local extra_args + local platform_args + local images + local tag + + extra_args=() + images=() + for image in "${IMAGES[@]}"; do + tag="${VERSION}" + if [[ "${STAGING_PREFIX}" != "" ]]; then + tag="${STAGING_PREFIX}-${VERSION}" + fi + extra_args+=("--tag=${image}:${tag}") + images+=("${image}:${tag}") + + if [[ "${#EXTRA_TAGS[@]}" -ne 0 ]]; then + for extra_tag in "${EXTRA_TAGS[@]}"; do + tag="${extra_tag}" + if [[ "${STAGING_PREFIX}" != "" ]]; then + tag="${STAGING_PREFIX}-${extra_tag}" + fi + extra_args+=("--tag=${image}:${tag}") + images+=("${image}:${tag}") + done + fi + done + + if [[ "${BASE_IMAGE}" != "" ]]; then + extra_args+=("--build-arg=BASE_IMAGE=${BASE_IMAGE}") + fi + + for platform in "${PLATFORMS[@]}"; do + extra_args+=("--platform=${platform}") + platform_args+=("--platform=${platform}") + done + + if [[ "${BUILDER}" == "nerdctl" ]]; then + build_with_nerdctl "${extra_args[@]}" + if [[ "${PUSH}" == "true" ]]; then + for image in "${images[@]}"; do + dry_run nerdctl push "${platform_args[@]}" "${image}" + done + fi + elif [[ "${BUILDER}" == "podman" ]]; then + build_with_podman "${extra_args[@]}" + if [[ "${PUSH}" == "true" ]]; then + for image in "${images[@]}"; do + dry_run podman push "${platform_args[@]}" "${image}" + done + fi + else + if [[ "${PUSH}" == "true" ]]; then + extra_args+=("--push") + else + extra_args+=("--load") + fi + build_with_docker "${extra_args[@]}" + fi +} + +function build_with_docker() { + local extra_args + extra_args=("$@") + dry_run docker buildx build \ + "${extra_args[@]}" \ + -f "${DOCKERFILE}" \ + . +} + +function build_with_nerdctl() { + local extra_args + extra_args=("$@") + dry_run nerdctl build \ + "${extra_args[@]}" \ + -f "${DOCKERFILE}" \ + . +} + +function build_with_podman() { + local extra_args + extra_args=("$@") + dry_run podman build \ + "${extra_args[@]}" \ + -f "${DOCKERFILE}" \ + . +} + +args "$@" + +cd "${ROOT_DIR}" && main diff --git a/kustomize/operator/controller.yaml b/kustomize/operator/controller.yaml new file mode 100644 index 000000000..28caa81c1 --- /dev/null +++ b/kustomize/operator/controller.yaml @@ -0,0 +1,6 @@ +apiVersion: operator.kwok.x-k8s.io/v1alpha1 +kind: Controller +metadata: + name: kwok-controller + namespace: kube-system +spec: {} diff --git a/kustomize/operator/crd/bases/operator.kwok.x-k8s.io_controllers.yaml b/kustomize/operator/crd/bases/operator.kwok.x-k8s.io_controllers.yaml new file mode 100644 index 000000000..dda7ed3c0 --- /dev/null +++ b/kustomize/operator/crd/bases/operator.kwok.x-k8s.io_controllers.yaml @@ -0,0 +1,128 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: controllers.operator.kwok.x-k8s.io +spec: + group: operator.kwok.x-k8s.io + names: + kind: Controller + listKind: ControllerList + plural: controllers + singular: controller + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Controller provides controller configuration for a single pod. + 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: Spec holds spec for controller + properties: + manages: + items: + description: TargetResourceRef specifies the kind and version of + the resource. + properties: + apiGroup: + default: v1 + description: APIGroup of the referent. + type: string + kind: + description: Kind of the referent. + type: string + name: + description: Name of the resource + type: string + namespace: + description: Namespace of the resource + type: string + required: + - kind + type: object + type: array + type: object + status: + description: Status holds status for controller + properties: + conditions: + description: Conditions holds conditions for controller + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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 + 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 + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/kustomize/operator/crd/kustomization.yaml b/kustomize/operator/crd/kustomization.yaml new file mode 100644 index 000000000..7247c86ab --- /dev/null +++ b/kustomize/operator/crd/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- bases/operator.kwok.x-k8s.io_controllers.yaml diff --git a/kustomize/operator/deployment.yaml b/kustomize/operator/deployment.yaml new file mode 100644 index 000000000..b0644b249 --- /dev/null +++ b/kustomize/operator/deployment.yaml @@ -0,0 +1,14 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kwok-operator +spec: + replicas: 1 + template: + spec: + containers: + - name: kwok-operator + image: registry.k8s.io/kwok/kwok-operator + imagePullPolicy: IfNotPresent + serviceAccountName: kwok-operator + restartPolicy: Always diff --git a/kustomize/operator/kustomization.yaml b/kustomize/operator/kustomization.yaml new file mode 100644 index 000000000..779133855 --- /dev/null +++ b/kustomize/operator/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: kube-system + +resources: +- crd +- rbac +- deployment.yaml + +labels: +- includeSelectors: true + pairs: + app: kwok-operator diff --git a/kustomize/operator/rbac/kustomization.yaml b/kustomize/operator/rbac/kustomization.yaml new file mode 100644 index 000000000..11a2ab55b --- /dev/null +++ b/kustomize/operator/rbac/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- service_account.yaml +- role.yaml +- role_binding.yaml diff --git a/kustomize/operator/rbac/role.yaml b/kustomize/operator/rbac/role.yaml new file mode 100644 index 000000000..9bf52cbd9 --- /dev/null +++ b/kustomize/operator/rbac/role.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kwok-operator +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments/scale + verbs: + - update +- apiGroups: + - kwok.x-k8s.io + resources: + - controllers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kwok.x-k8s.io + resources: + - controllers/status + verbs: + - patch + - update diff --git a/kustomize/operator/rbac/role_binding.yaml b/kustomize/operator/rbac/role_binding.yaml new file mode 100644 index 000000000..70f2c7b10 --- /dev/null +++ b/kustomize/operator/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kwok-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kwok-operator +subjects: +- kind: ServiceAccount + name: kwok-operator + namespace: kube-system diff --git a/kustomize/operator/rbac/service_account.yaml b/kustomize/operator/rbac/service_account.yaml new file mode 100644 index 000000000..74289392f --- /dev/null +++ b/kustomize/operator/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kwok-operator + namespace: kube-system diff --git a/pkg/apis/internalversion/controller_types.go b/pkg/apis/internalversion/controller_types.go new file mode 100644 index 000000000..a8a78ea53 --- /dev/null +++ b/pkg/apis/internalversion/controller_types.go @@ -0,0 +1,49 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internalversion + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Controller provides controller configuration for a single pod. +type Controller struct { + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta + // Spec holds spec for controller + Spec ControllerSpec +} + +// ControllerSpec holds spec for controller. +type ControllerSpec struct { + Manages []TargetResourceRef +} + +// TargetResourceRef specifies the kind and version of the resource. +type TargetResourceRef struct { + // APIGroup of the referent. + // +default="v1" + // +kubebuilder:default="v1" + APIGroup string + // Kind of the referent. + Kind string + // Namespace of the resource + Namespace *string + // Name of the resource + Name *string +} diff --git a/pkg/apis/internalversion/doc.go b/pkg/apis/internalversion/doc.go index f113b4c26..223a5e53a 100644 --- a/pkg/apis/internalversion/doc.go +++ b/pkg/apis/internalversion/doc.go @@ -17,6 +17,7 @@ limitations under the License. // +k8s:deepcopy-gen=package // +k8s:defaulter-gen=TypeMeta // +k8s:conversion-gen=sigs.k8s.io/kwok/pkg/apis/v1alpha1 +// +k8s:conversion-gen=sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1 // +k8s:conversion-gen=sigs.k8s.io/kwok/pkg/apis/config/v1alpha1 // Package internalversion implements the internal apiVersion of kwok's configuration diff --git a/pkg/apis/internalversion/zz_generated.conversion.go b/pkg/apis/internalversion/zz_generated.conversion.go index b4d243b9b..79aa707c7 100644 --- a/pkg/apis/internalversion/zz_generated.conversion.go +++ b/pkg/apis/internalversion/zz_generated.conversion.go @@ -30,6 +30,7 @@ import ( conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" configv1alpha1 "sigs.k8s.io/kwok/pkg/apis/config/v1alpha1" + operatorv1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" v1alpha1 "sigs.k8s.io/kwok/pkg/apis/v1alpha1" ) @@ -200,6 +201,26 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Controller)(nil), (*operatorv1alpha1.Controller)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_Controller_To_v1alpha1_Controller(a.(*Controller), b.(*operatorv1alpha1.Controller), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*operatorv1alpha1.Controller)(nil), (*Controller)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Controller_To_internalversion_Controller(a.(*operatorv1alpha1.Controller), b.(*Controller), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ControllerSpec)(nil), (*operatorv1alpha1.ControllerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec(a.(*ControllerSpec), b.(*operatorv1alpha1.ControllerSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*operatorv1alpha1.ControllerSpec)(nil), (*ControllerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec(a.(*operatorv1alpha1.ControllerSpec), b.(*ControllerSpec), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Env)(nil), (*configv1alpha1.Env)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_internalversion_Env_To_v1alpha1_Env(a.(*Env), b.(*configv1alpha1.Env), scope) }); err != nil { @@ -645,6 +666,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*TargetResourceRef)(nil), (*operatorv1alpha1.TargetResourceRef)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_internalversion_TargetResourceRef_To_v1alpha1_TargetResourceRef(a.(*TargetResourceRef), b.(*operatorv1alpha1.TargetResourceRef), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*operatorv1alpha1.TargetResourceRef)(nil), (*TargetResourceRef)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TargetResourceRef_To_internalversion_TargetResourceRef(a.(*operatorv1alpha1.TargetResourceRef), b.(*TargetResourceRef), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Volume)(nil), (*configv1alpha1.Volume)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_internalversion_Volume_To_v1alpha1_Volume(a.(*Volume), b.(*configv1alpha1.Volume), scope) }); err != nil { @@ -1199,6 +1230,54 @@ func Convert_v1alpha1_ComponentPatches_To_internalversion_ComponentPatches(in *c return autoConvert_v1alpha1_ComponentPatches_To_internalversion_ComponentPatches(in, out, s) } +func autoConvert_internalversion_Controller_To_v1alpha1_Controller(in *Controller, out *operatorv1alpha1.Controller, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_internalversion_Controller_To_v1alpha1_Controller is an autogenerated conversion function. +func Convert_internalversion_Controller_To_v1alpha1_Controller(in *Controller, out *operatorv1alpha1.Controller, s conversion.Scope) error { + return autoConvert_internalversion_Controller_To_v1alpha1_Controller(in, out, s) +} + +func autoConvert_v1alpha1_Controller_To_internalversion_Controller(in *operatorv1alpha1.Controller, out *Controller, s conversion.Scope) error { + // INFO: in.TypeMeta opted out of conversion generation + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + // INFO: in.Status opted out of conversion generation + return nil +} + +// Convert_v1alpha1_Controller_To_internalversion_Controller is an autogenerated conversion function. +func Convert_v1alpha1_Controller_To_internalversion_Controller(in *operatorv1alpha1.Controller, out *Controller, s conversion.Scope) error { + return autoConvert_v1alpha1_Controller_To_internalversion_Controller(in, out, s) +} + +func autoConvert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec(in *ControllerSpec, out *operatorv1alpha1.ControllerSpec, s conversion.Scope) error { + out.Manages = *(*[]operatorv1alpha1.TargetResourceRef)(unsafe.Pointer(&in.Manages)) + return nil +} + +// Convert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec is an autogenerated conversion function. +func Convert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec(in *ControllerSpec, out *operatorv1alpha1.ControllerSpec, s conversion.Scope) error { + return autoConvert_internalversion_ControllerSpec_To_v1alpha1_ControllerSpec(in, out, s) +} + +func autoConvert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec(in *operatorv1alpha1.ControllerSpec, out *ControllerSpec, s conversion.Scope) error { + out.Manages = *(*[]TargetResourceRef)(unsafe.Pointer(&in.Manages)) + return nil +} + +// Convert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec is an autogenerated conversion function. +func Convert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec(in *operatorv1alpha1.ControllerSpec, out *ControllerSpec, s conversion.Scope) error { + return autoConvert_v1alpha1_ControllerSpec_To_internalversion_ControllerSpec(in, out, s) +} + func autoConvert_internalversion_Env_To_v1alpha1_Env(in *Env, out *configv1alpha1.Env, s conversion.Scope) error { out.Name = in.Name out.Value = in.Value @@ -2664,6 +2743,32 @@ func Convert_v1alpha1_StageSpec_To_internalversion_StageSpec(in *v1alpha1.StageS return autoConvert_v1alpha1_StageSpec_To_internalversion_StageSpec(in, out, s) } +func autoConvert_internalversion_TargetResourceRef_To_v1alpha1_TargetResourceRef(in *TargetResourceRef, out *operatorv1alpha1.TargetResourceRef, s conversion.Scope) error { + out.APIGroup = in.APIGroup + out.Kind = in.Kind + out.Namespace = (*string)(unsafe.Pointer(in.Namespace)) + out.Name = (*string)(unsafe.Pointer(in.Name)) + return nil +} + +// Convert_internalversion_TargetResourceRef_To_v1alpha1_TargetResourceRef is an autogenerated conversion function. +func Convert_internalversion_TargetResourceRef_To_v1alpha1_TargetResourceRef(in *TargetResourceRef, out *operatorv1alpha1.TargetResourceRef, s conversion.Scope) error { + return autoConvert_internalversion_TargetResourceRef_To_v1alpha1_TargetResourceRef(in, out, s) +} + +func autoConvert_v1alpha1_TargetResourceRef_To_internalversion_TargetResourceRef(in *operatorv1alpha1.TargetResourceRef, out *TargetResourceRef, s conversion.Scope) error { + out.APIGroup = in.APIGroup + out.Kind = in.Kind + out.Namespace = (*string)(unsafe.Pointer(in.Namespace)) + out.Name = (*string)(unsafe.Pointer(in.Name)) + return nil +} + +// Convert_v1alpha1_TargetResourceRef_To_internalversion_TargetResourceRef is an autogenerated conversion function. +func Convert_v1alpha1_TargetResourceRef_To_internalversion_TargetResourceRef(in *operatorv1alpha1.TargetResourceRef, out *TargetResourceRef, s conversion.Scope) error { + return autoConvert_v1alpha1_TargetResourceRef_To_internalversion_TargetResourceRef(in, out, s) +} + func autoConvert_internalversion_Volume_To_v1alpha1_Volume(in *Volume, out *configv1alpha1.Volume, s conversion.Scope) error { out.Name = in.Name if err := v1.Convert_bool_To_Pointer_bool(&in.ReadOnly, &out.ReadOnly, s); err != nil { diff --git a/pkg/apis/internalversion/zz_generated.deepcopy.go b/pkg/apis/internalversion/zz_generated.deepcopy.go index fb56747cb..ea8967775 100644 --- a/pkg/apis/internalversion/zz_generated.deepcopy.go +++ b/pkg/apis/internalversion/zz_generated.deepcopy.go @@ -420,6 +420,47 @@ func (in *ComponentPatches) DeepCopy() *ComponentPatches { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Controller) DeepCopyInto(out *Controller) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Controller. +func (in *Controller) DeepCopy() *Controller { + if in == nil { + return nil + } + out := new(Controller) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerSpec) DeepCopyInto(out *ControllerSpec) { + *out = *in + if in.Manages != nil { + in, out := &in.Manages, &out.Manages + *out = make([]TargetResourceRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerSpec. +func (in *ControllerSpec) DeepCopy() *ControllerSpec { + if in == nil { + return nil + } + out := new(ControllerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Env) DeepCopyInto(out *Env) { *out = *in @@ -1499,6 +1540,32 @@ func (in *StageSpec) DeepCopy() *StageSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetResourceRef) DeepCopyInto(out *TargetResourceRef) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetResourceRef. +func (in *TargetResourceRef) DeepCopy() *TargetResourceRef { + if in == nil { + return nil + } + out := new(TargetResourceRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Volume) DeepCopyInto(out *Volume) { *out = *in diff --git a/pkg/apis/operator/v1alpha1/condition.go b/pkg/apis/operator/v1alpha1/condition.go new file mode 100644 index 000000000..89cdf8f0d --- /dev/null +++ b/pkg/apis/operator/v1alpha1/condition.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Condition contains details for one aspect of the current state of this API Resource. +type Condition struct { + // 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) + // +kubebuilder:validation:Required + // +kubebuilder:validation: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])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + // Status of the condition + // +kubebuilder:validation:Required + Status ConditionStatus `json:"status"` + // 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. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // 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. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + // Message is a human readable message indicating details about the transition. + // This may be an empty string. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} + +// ConditionStatus is the status of a condition. +// +enum +type ConditionStatus string + +const ( + // ConditionTrue means a resource is in the condition. + ConditionTrue ConditionStatus = "True" + // ConditionFalse means a resource is not in the condition. + ConditionFalse ConditionStatus = "False" + // ConditionUnknown means kubernetes can't decide if a resource is in the condition or not. + ConditionUnknown ConditionStatus = "Unknown" +) diff --git a/pkg/apis/operator/v1alpha1/controller_types.go b/pkg/apis/operator/v1alpha1/controller_types.go new file mode 100644 index 000000000..d0e130aa8 --- /dev/null +++ b/pkg/apis/operator/v1alpha1/controller_types.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // ControllerKind is the kind of the Logs. + ControllerKind = "Controller" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +// +kubebuilder:subresource:status +// +kubebuilder:rbac:groups=kwok.x-k8s.io,resources=controllers,verbs=create;delete;get;list;patch;update;watch +// +kubebuilder:rbac:groups=kwok.x-k8s.io,resources=controllers/status,verbs=update;patch + +// Controller provides controller configuration for a single pod. +type Controller struct { + //+k8s:conversion-gen=false + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata"` + // Spec holds spec for controller + Spec ControllerSpec `json:"spec"` + // Status holds status for controller + //+k8s:conversion-gen=false + Status ControllerStatus `json:"status,omitempty"` +} + +// ControllerStatus holds status for controller +type ControllerStatus struct { + // Conditions holds conditions for controller + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// ControllerSpec holds spec for controller. +type ControllerSpec struct { + Manages []TargetResourceRef `json:"manages,omitempty"` +} + +// TargetResourceRef specifies the kind and version of the resource. +type TargetResourceRef struct { + // APIGroup of the referent. + // +default="v1" + // +kubebuilder:default="v1" + APIGroup string `json:"apiGroup,omitempty"` + // Kind of the referent. + Kind string `json:"kind"` + // Namespace of the resource + Namespace *string `json:"namespace,omitempty"` + // Name of the resource + Name *string `json:"name,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// ControllerList contains a list of Controller +type ControllerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Controller `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Controller{}, &ControllerList{}) +} diff --git a/pkg/apis/operator/v1alpha1/doc.go b/pkg/apis/operator/v1alpha1/doc.go new file mode 100644 index 000000000..21fac0bc9 --- /dev/null +++ b/pkg/apis/operator/v1alpha1/doc.go @@ -0,0 +1,26 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package +// +k8s:defaulter-gen=TypeMeta +// +groupName=operator.kwok.x-k8s.io + +// +kubebuilder:rbac:groups="",resources=pods,verbs=create;delete;get +// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch +// +kubebuilder:rbac:groups="apps",resources=deployments/scale,verbs=update + +// Package v1alpha1 implements the v1alpha1 apiVersion of kwok's operator +package v1alpha1 diff --git a/pkg/apis/operator/v1alpha1/groupversion_info.go b/pkg/apis/operator/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..dc3f5ea26 --- /dev/null +++ b/pkg/apis/operator/v1alpha1/groupversion_info.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +kubebuilder:object:generate=true +// +groupName=operator.kwok.x-k8s.io + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{ + Group: "operator.kwok.x-k8s.io", + Version: "v1alpha1", + } + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/apis/operator/v1alpha1/register.go b/pkg/apis/operator/v1alpha1/register.go new file mode 100644 index 000000000..d595f83df --- /dev/null +++ b/pkg/apis/operator/v1alpha1/register.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = GroupVersion + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..ef91c99ed --- /dev/null +++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,176 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Controller) DeepCopyInto(out *Controller) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Controller. +func (in *Controller) DeepCopy() *Controller { + if in == nil { + return nil + } + out := new(Controller) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Controller) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerList) DeepCopyInto(out *ControllerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Controller, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerList. +func (in *ControllerList) DeepCopy() *ControllerList { + if in == nil { + return nil + } + out := new(ControllerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerSpec) DeepCopyInto(out *ControllerSpec) { + *out = *in + if in.Manages != nil { + in, out := &in.Manages, &out.Manages + *out = make([]TargetResourceRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerSpec. +func (in *ControllerSpec) DeepCopy() *ControllerSpec { + if in == nil { + return nil + } + out := new(ControllerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerStatus) DeepCopyInto(out *ControllerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerStatus. +func (in *ControllerStatus) DeepCopy() *ControllerStatus { + if in == nil { + return nil + } + out := new(ControllerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetResourceRef) DeepCopyInto(out *TargetResourceRef) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetResourceRef. +func (in *TargetResourceRef) DeepCopy() *TargetResourceRef { + if in == nil { + return nil + } + out := new(TargetResourceRef) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/operator/v1alpha1/zz_generated.defaults.go b/pkg/apis/operator/v1alpha1/zz_generated.defaults.go new file mode 100644 index 000000000..6521b4f48 --- /dev/null +++ b/pkg/apis/operator/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,51 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&Controller{}, func(obj interface{}) { SetObjectDefaults_Controller(obj.(*Controller)) }) + scheme.AddTypeDefaultingFunc(&ControllerList{}, func(obj interface{}) { SetObjectDefaults_ControllerList(obj.(*ControllerList)) }) + return nil +} + +func SetObjectDefaults_Controller(in *Controller) { + for i := range in.Spec.Manages { + a := &in.Spec.Manages[i] + if a.APIGroup == "" { + a.APIGroup = "v1" + } + } +} + +func SetObjectDefaults_ControllerList(in *ControllerList) { + for i := range in.Items { + a := &in.Items[i] + SetObjectDefaults_Controller(a) + } +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index 9eb643fe1..8c801ae50 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -26,17 +26,20 @@ import ( rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" kwokv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/apis/v1alpha1" + operatorv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/operator/v1alpha1" ) type Interface interface { Discovery() discovery.DiscoveryInterface KwokV1alpha1() kwokv1alpha1.KwokV1alpha1Interface + OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient - kwokV1alpha1 *kwokv1alpha1.KwokV1alpha1Client + kwokV1alpha1 *kwokv1alpha1.KwokV1alpha1Client + operatorV1alpha1 *operatorv1alpha1.OperatorV1alpha1Client } // KwokV1alpha1 retrieves the KwokV1alpha1Client @@ -44,6 +47,11 @@ func (c *Clientset) KwokV1alpha1() kwokv1alpha1.KwokV1alpha1Interface { return c.kwokV1alpha1 } +// OperatorV1alpha1 retrieves the OperatorV1alpha1Client +func (c *Clientset) OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface { + return c.operatorV1alpha1 +} + // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -92,6 +100,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.operatorV1alpha1, err = operatorv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { @@ -114,6 +126,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { func New(c rest.Interface) *Clientset { var cs Clientset cs.kwokV1alpha1 = kwokv1alpha1.New(c) + cs.operatorV1alpha1 = operatorv1alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 919b2d874..bf673c763 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -27,6 +27,8 @@ import ( clientset "sigs.k8s.io/kwok/pkg/client/clientset/versioned" kwokv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/apis/v1alpha1" fakekwokv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake" + operatorv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/operator/v1alpha1" + fakeoperatorv1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. @@ -87,3 +89,8 @@ var ( func (c *Clientset) KwokV1alpha1() kwokv1alpha1.KwokV1alpha1Interface { return &fakekwokv1alpha1.FakeKwokV1alpha1{Fake: &c.Fake} } + +// OperatorV1alpha1 retrieves the OperatorV1alpha1Client +func (c *Clientset) OperatorV1alpha1() operatorv1alpha1.OperatorV1alpha1Interface { + return &fakeoperatorv1alpha1.FakeOperatorV1alpha1{Fake: &c.Fake} +} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 489b8a2ca..2cef8f1c3 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -24,6 +24,7 @@ import ( schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + operatorv1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" kwokv1alpha1 "sigs.k8s.io/kwok/pkg/apis/v1alpha1" ) @@ -32,6 +33,7 @@ var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ kwokv1alpha1.AddToScheme, + operatorv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 4f0832e11..a5f7a7c6f 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -24,6 +24,7 @@ import ( schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + operatorv1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" kwokv1alpha1 "sigs.k8s.io/kwok/pkg/apis/v1alpha1" ) @@ -32,6 +33,7 @@ var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ kwokv1alpha1.AddToScheme, + operatorv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/controller.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/controller.go new file mode 100644 index 000000000..e181f2bb5 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/controller.go @@ -0,0 +1,69 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" + v1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" + scheme "sigs.k8s.io/kwok/pkg/client/clientset/versioned/scheme" +) + +// ControllersGetter has a method to return a ControllerInterface. +// A group's client should implement this interface. +type ControllersGetter interface { + Controllers(namespace string) ControllerInterface +} + +// ControllerInterface has methods to work with Controller resources. +type ControllerInterface interface { + Create(ctx context.Context, controller *v1alpha1.Controller, opts v1.CreateOptions) (*v1alpha1.Controller, error) + Update(ctx context.Context, controller *v1alpha1.Controller, opts v1.UpdateOptions) (*v1alpha1.Controller, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, controller *v1alpha1.Controller, opts v1.UpdateOptions) (*v1alpha1.Controller, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Controller, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ControllerList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Controller, err error) + ControllerExpansion +} + +// controllers implements ControllerInterface +type controllers struct { + *gentype.ClientWithList[*v1alpha1.Controller, *v1alpha1.ControllerList] +} + +// newControllers returns a Controllers +func newControllers(c *OperatorV1alpha1Client, namespace string) *controllers { + return &controllers{ + gentype.NewClientWithList[*v1alpha1.Controller, *v1alpha1.ControllerList]( + "controllers", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1alpha1.Controller { return &v1alpha1.Controller{} }, + func() *v1alpha1.ControllerList { return &v1alpha1.ControllerList{} }), + } +} diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/doc.go new file mode 100644 index 000000000..df51baa4d --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_controller.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_controller.go new file mode 100644 index 000000000..32a8c7070 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_controller.go @@ -0,0 +1,147 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" + v1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" +) + +// FakeControllers implements ControllerInterface +type FakeControllers struct { + Fake *FakeOperatorV1alpha1 + ns string +} + +var controllersResource = v1alpha1.SchemeGroupVersion.WithResource("controllers") + +var controllersKind = v1alpha1.SchemeGroupVersion.WithKind("Controller") + +// Get takes name of the controller, and returns the corresponding controller object, and an error if there is any. +func (c *FakeControllers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Controller, err error) { + emptyResult := &v1alpha1.Controller{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(controllersResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.Controller), err +} + +// List takes label and field selectors, and returns the list of Controllers that match those selectors. +func (c *FakeControllers) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ControllerList, err error) { + emptyResult := &v1alpha1.ControllerList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(controllersResource, controllersKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ControllerList{ListMeta: obj.(*v1alpha1.ControllerList).ListMeta} + for _, item := range obj.(*v1alpha1.ControllerList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested controllers. +func (c *FakeControllers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(controllersResource, c.ns, opts)) + +} + +// Create takes the representation of a controller and creates it. Returns the server's representation of the controller, and an error, if there is any. +func (c *FakeControllers) Create(ctx context.Context, controller *v1alpha1.Controller, opts v1.CreateOptions) (result *v1alpha1.Controller, err error) { + emptyResult := &v1alpha1.Controller{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(controllersResource, c.ns, controller, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.Controller), err +} + +// Update takes the representation of a controller and updates it. Returns the server's representation of the controller, and an error, if there is any. +func (c *FakeControllers) Update(ctx context.Context, controller *v1alpha1.Controller, opts v1.UpdateOptions) (result *v1alpha1.Controller, err error) { + emptyResult := &v1alpha1.Controller{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(controllersResource, c.ns, controller, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.Controller), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeControllers) UpdateStatus(ctx context.Context, controller *v1alpha1.Controller, opts v1.UpdateOptions) (result *v1alpha1.Controller, err error) { + emptyResult := &v1alpha1.Controller{} + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceActionWithOptions(controllersResource, "status", c.ns, controller, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.Controller), err +} + +// Delete takes name of the controller and deletes it. Returns an error if one occurs. +func (c *FakeControllers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(controllersResource, c.ns, name, opts), &v1alpha1.Controller{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeControllers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(controllersResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ControllerList{}) + return err +} + +// Patch applies the patch and returns the patched controller. +func (c *FakeControllers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Controller, err error) { + emptyResult := &v1alpha1.Controller{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(controllersResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.Controller), err +} diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go new file mode 100644 index 000000000..94999820f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" + v1alpha1 "sigs.k8s.io/kwok/pkg/client/clientset/versioned/typed/operator/v1alpha1" +) + +type FakeOperatorV1alpha1 struct { + *testing.Fake +} + +func (c *FakeOperatorV1alpha1) Controllers(namespace string) v1alpha1.ControllerInterface { + return &FakeControllers{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeOperatorV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..54477870f --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type ControllerExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go new file mode 100644 index 000000000..cd90ba051 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go @@ -0,0 +1,107 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + rest "k8s.io/client-go/rest" + v1alpha1 "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" + "sigs.k8s.io/kwok/pkg/client/clientset/versioned/scheme" +) + +type OperatorV1alpha1Interface interface { + RESTClient() rest.Interface + ControllersGetter +} + +// OperatorV1alpha1Client is used to interact with features provided by the operator.kwok.x-k8s.io group. +type OperatorV1alpha1Client struct { + restClient rest.Interface +} + +func (c *OperatorV1alpha1Client) Controllers(namespace string) ControllerInterface { + return newControllers(c, namespace) +} + +// NewForConfig creates a new OperatorV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*OperatorV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new OperatorV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*OperatorV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &OperatorV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new OperatorV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *OperatorV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new OperatorV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *OperatorV1alpha1Client { + return &OperatorV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *OperatorV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/operator/cmd/root.go b/pkg/operator/cmd/root.go new file mode 100644 index 000000000..8a7f2fd8e --- /dev/null +++ b/pkg/operator/cmd/root.go @@ -0,0 +1,125 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package cmd defines a root command for the kwok. +package cmd + +import ( + "context" + "os" + + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + + "sigs.k8s.io/kwok/pkg/client/clientset/versioned" + "sigs.k8s.io/kwok/pkg/log" + "sigs.k8s.io/kwok/pkg/operator/controllers" + "sigs.k8s.io/kwok/pkg/utils/client" + "sigs.k8s.io/kwok/pkg/utils/kubeconfig" + "sigs.k8s.io/kwok/pkg/utils/path" + "sigs.k8s.io/kwok/pkg/utils/version" +) + +type flagpole struct { + Kubeconfig string + Master string + + SourceControllerNamespace string + SourceControllerName string +} + +// NewCommand returns a new cobra.Command for root +func NewCommand(ctx context.Context) *cobra.Command { + flags := &flagpole{} + cmd := &cobra.Command{ + Args: cobra.NoArgs, + Use: "kwok-operator", + Short: "kwok-operator is a operator for kwok-controller", + SilenceUsage: true, + SilenceErrors: true, + Version: version.DisplayVersion(), + RunE: func(cmd *cobra.Command, args []string) error { + return runE(cmd.Context(), flags) + }, + } + + flags.Kubeconfig = path.RelFromHome(kubeconfig.GetRecommendedKubeconfigPath()) + + cmd.Flags().StringVar(&flags.Kubeconfig, "kubeconfig", flags.Kubeconfig, "Path to the kubeconfig file to use") + cmd.Flags().StringVar(&flags.Master, "master", flags.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).") + + cmd.Flags().StringVar(&flags.SourceControllerName, "controller-name", "kwok-controller", "The name of the controller to use for the source controller") + cmd.Flags().StringVar(&flags.SourceControllerNamespace, "controller-namespace", "kube-system", "The namespace of the controller to use for the source controller") + return cmd +} + +func runE(ctx context.Context, flags *flagpole) error { + logger := log.FromContext(ctx) + + if flags.Kubeconfig != "" { + var err error + flags.Kubeconfig, err = path.Expand(flags.Kubeconfig) + if err != nil { + return err + } + f, err := os.Stat(flags.Kubeconfig) + if err != nil || f.IsDir() { + logger.Warn("Failed to get kubeconfig file or it is a directory", "kubeconfig", flags.Kubeconfig) + flags.Kubeconfig = "" + } + } + + if flags.Kubeconfig == "" && flags.Master == "" { + logger.Warn("Neither --kubeconfig nor --master was specified") + logger.Info("Using the inClusterConfig") + } + clientset, err := client.NewClientset(flags.Master, flags.Kubeconfig) + if err != nil { + return err + } + + restConfig, err := clientset.ToRESTConfig() + if err != nil { + return err + } + + typedClient, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return err + } + typedKwokClient, err := versioned.NewForConfig(restConfig) + if err != nil { + return err + } + + ctr, err := controllers.NewController(controllers.ControllerConfig{ + TypedClient: typedClient, + TypedKwokClient: typedKwokClient, + SourceNamespace: flags.SourceControllerNamespace, + SourceName: flags.SourceControllerName, + }) + if err != nil { + return err + } + + err = ctr.Start(ctx) + if err != nil { + return err + } + + <-ctx.Done() + return nil +} diff --git a/pkg/operator/controllers/controller.go b/pkg/operator/controllers/controller.go new file mode 100644 index 000000000..bf9650925 --- /dev/null +++ b/pkg/operator/controllers/controller.go @@ -0,0 +1,202 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "reflect" + + autoscalingv1 "k8s.io/api/autoscaling/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "sigs.k8s.io/kwok/pkg/apis/operator/v1alpha1" + "sigs.k8s.io/kwok/pkg/client/clientset/versioned" + "sigs.k8s.io/kwok/pkg/log" + "sigs.k8s.io/kwok/pkg/utils/format" + "sigs.k8s.io/kwok/pkg/utils/informer" + "sigs.k8s.io/kwok/pkg/utils/sets" +) + +type Controller struct { + typedClient kubernetes.Interface + typedKwokClient versioned.Interface + sourceNamespace string + sourceName string + + podTemplate *corev1.PodTemplateSpec + controllersInformer *informer.Informer[*v1alpha1.Controller, *v1alpha1.ControllerList] + controllersCacheGetter informer.Getter[*v1alpha1.Controller] + controllersChan chan informer.Event[*v1alpha1.Controller] +} + +// ControllerConfig is the configuration for the controller +type ControllerConfig struct { + TypedClient kubernetes.Interface + TypedKwokClient versioned.Interface + + SourceNamespace string + SourceName string +} + +func NewController(conf ControllerConfig) (*Controller, error) { + c := &Controller{ + typedClient: conf.TypedClient, + typedKwokClient: conf.TypedKwokClient, + sourceNamespace: conf.SourceNamespace, + sourceName: conf.SourceName, + } + + return c, nil +} + +func (c *Controller) Start(ctx context.Context) error { + c.controllersInformer = informer.NewInformer[*v1alpha1.Controller, *v1alpha1.ControllerList](c.typedKwokClient.OperatorV1alpha1().Controllers("")) + c.controllersChan = make(chan informer.Event[*v1alpha1.Controller], 1) + controllersCacheGetter, err := c.controllersInformer.WatchWithCache(ctx, informer.Option{}, c.controllersChan) + if err != nil { + return err + } + c.controllersCacheGetter = controllersCacheGetter + + go c.syncWorker(ctx) + return nil +} + +func (c *Controller) scaleDeployment(ctx context.Context, replicas int32) error { + deploy, err := c.typedClient.AppsV1(). + Deployments(c.sourceNamespace). + Get(ctx, c.sourceName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("getting deployment: %w", err) + } + c.podTemplate = &deploy.Spec.Template + + if *deploy.Spec.Replicas == replicas { + return nil + } + + _, err = c.typedClient.AppsV1(). + Deployments(c.sourceNamespace). + UpdateScale(ctx, c.sourceName, &autoscalingv1.Scale{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.sourceName, + }, + Spec: autoscalingv1.ScaleSpec{ + Replicas: replicas, + }, + }, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("updating deployment scale: %w", err) + } + return nil +} + +func (c *Controller) syncWorker(ctx context.Context) { + s := sets.Sets[log.ObjectRef]{} + + logger := log.FromContext(ctx) + for { + select { + case <-ctx.Done(): + logger.Debug("Stop controller sync worker") + return + case event := <-c.controllersChan: + cc := event.Object + + p := c.typedClient.CoreV1(). + Pods(cc.Namespace) + + if event.Type == informer.Deleted { + err := p.Delete(ctx, cc.GetName(), metav1.DeleteOptions{}) + if err != nil { + logger.Error("can't delete the origin pod", err) + } + s.Delete(log.KObj(cc)) + if s.Len() == 0 { + err := c.scaleDeployment(ctx, 1) + if err != nil { + logger.Error("can't sync deployment", err) + continue + } + } + continue + } + if s.Len() == 0 { + err := c.scaleDeployment(ctx, 0) + if err != nil { + logger.Error("can't sync deployment", err) + continue + } + } + s.Insert(log.KObj(cc)) + + pod := controllerPod(cc, c.podTemplate.Spec) + + oriPod, err := p.Get(ctx, cc.GetName(), metav1.GetOptions{}) + if err != nil { + if !apierrors.IsNotFound(err) { + logger.Error("can't get the origin pod", err) + continue + } + } else { + if reflect.DeepEqual(pod.Spec, oriPod.Spec) { + continue + } + + err = p.Delete(ctx, cc.GetName(), metav1.DeleteOptions{ + GracePeriodSeconds: format.Ptr[int64](0), + }) + if err != nil { + logger.Error("can't delete the origin pod", err) + continue + } + } + + _, err = p.Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + logger.Error("can't create the pod", err) + continue + } + } + } +} + +func controllerPod(c *v1alpha1.Controller, ps corev1.PodSpec) *corev1.Pod { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.Name, + Namespace: c.Namespace, + Labels: c.Labels, + Annotations: c.Annotations, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: v1alpha1.GroupVersion.String(), + Kind: v1alpha1.ControllerKind, + Name: c.Name, + UID: c.ObjectMeta.UID, + Controller: format.Ptr(true), + }, + }, + }, + Spec: ps, + } + return &pod +} diff --git a/pkg/operator/controllers/doc.go b/pkg/operator/controllers/doc.go new file mode 100644 index 000000000..ee82b3adf --- /dev/null +++ b/pkg/operator/controllers/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package controllers is the package that contains the operator logic. +package controllers diff --git a/site/content/en/docs/generated/apis.md b/site/content/en/docs/generated/apis.md index 259a5c3d4..404fa6939 100644 --- a/site/content/en/docs/generated/apis.md +++ b/site/content/en/docs/generated/apis.md @@ -14,6 +14,9 @@ bookToc: false
  • kwok.x-k8s.io/v1alpha1
  • +
  • +operator.kwok.x-k8s.io/v1alpha1 +
  • action.kwok.x-k8s.io/v1alpha1 @@ -1799,6 +1802,109 @@ StageStatus +

    +operator.kwok.x-k8s.io/v1alpha1 + # +

    +
    +

    Package v1alpha1 implements the v1alpha1 apiVersion of kwok’s operator

    +
    +Resource Types: + +

    +Controller + # +

    +

    +

    Controller provides controller configuration for a single pod.

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +apiVersion +string + + +operator.kwok.x-k8s.io/v1alpha1 + +
    +kind +string +Controller
    +metadata + + +Kubernetes meta/v1.ObjectMeta + + + +

    Standard list metadata. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

    +Refer to the Kubernetes API documentation for the fields of the +metadata field. +
    +spec + + +ControllerSpec + + + +

    Spec holds spec for controller

    + + + + + +
    +manages + + +[]TargetResourceRef + + + +
    +
    +status + + +ControllerStatus + + + +

    Status holds status for controller

    +

    References # @@ -6731,3 +6837,261 @@ StageStatus +

    +Condition + # +

    +

    +Appears on: +ControllerStatus +

    +

    +

    Condition contains details for one aspect of the current state of this API Resource.

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +type + +string + + +

    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)

    +
    +status + + +ConditionStatus + + + +

    Status of the condition

    +
    +lastTransitionTime + + +Kubernetes meta/v1.Time + + + +

    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.

    +
    +reason + +string + + +

    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.

    +
    +message + +string + + +

    Message is a human readable message indicating details about the transition. +This may be an empty string.

    +
    +

    +ConditionStatus +(string alias) + # +

    +

    +Appears on: +Condition +

    +

    +

    ConditionStatus is the status of a condition.

    +

    + + + + + + + + + + + + + + + + + + + + + +
    ValueDescription
    "False"

    ConditionFalse means a resource is not in the condition.

    +
    "True"

    ConditionTrue means a resource is in the condition.

    +
    "Unknown"

    ConditionUnknown means kubernetes can’t decide if a resource is in the condition or not.

    +
    +

    +ControllerSpec + # +

    +

    +Appears on: +Controller +

    +

    +

    ControllerSpec holds spec for controller.

    +

    + + + + + + + + + + + + + +
    FieldDescription
    +manages + + +[]TargetResourceRef + + + +
    +

    +ControllerStatus + # +

    +

    +Appears on: +Controller +

    +

    +

    ControllerStatus holds status for controller

    +

    + + + + + + + + + + + + + +
    FieldDescription
    +conditions + + +[]Condition + + + +

    Conditions holds conditions for controller

    +
    +

    +TargetResourceRef + # +

    +

    +Appears on: +ControllerSpec +

    +

    +

    TargetResourceRef specifies the kind and version of the resource.

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +apiGroup + +string + + +

    APIGroup of the referent.

    +
    +kind + +string + + +

    Kind of the referent.

    +
    +namespace + +string + + +

    Namespace of the resource

    +
    +name + +string + + +

    Name of the resource

    +
    diff --git a/test/release/testdata/build-with-push-bucket-staging.txt b/test/release/testdata/build-with-push-bucket-staging.txt index 551711a2e..87eb273b4 100644 --- a/test/release/testdata/build-with-push-bucket-staging.txt +++ b/test/release/testdata/build-with-push-bucket-staging.txt @@ -2,3 +2,5 @@ GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Versio gsutil cp -P ./bin///kwok bucket/releases/staging-prefix-/bin///kwok GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwokctl ./cmd/kwokctl gsutil cp -P ./bin///kwokctl bucket/releases/staging-prefix-/bin///kwokctl +GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin///kwok-operator bucket/releases/staging-prefix-/bin///kwok-operator diff --git a/test/release/testdata/build-with-push-bucket.txt b/test/release/testdata/build-with-push-bucket.txt index 4089b2612..11697231b 100644 --- a/test/release/testdata/build-with-push-bucket.txt +++ b/test/release/testdata/build-with-push-bucket.txt @@ -2,3 +2,5 @@ GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Versio gsutil cp -P ./bin///kwok bucket/releases//bin///kwok GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwokctl ./cmd/kwokctl gsutil cp -P ./bin///kwokctl bucket/releases//bin///kwokctl +GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin///kwok-operator bucket/releases//bin///kwok-operator diff --git a/test/release/testdata/build-with-push-ghrelease.txt b/test/release/testdata/build-with-push-ghrelease.txt index d2e9bf307..7b9b9acd2 100644 --- a/test/release/testdata/build-with-push-ghrelease.txt +++ b/test/release/testdata/build-with-push-ghrelease.txt @@ -4,3 +4,6 @@ gh -R ghrelease release upload kwok-- GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwokctl ./cmd/kwokctl cp ./bin///kwokctl kwokctl-- gh -R ghrelease release upload kwokctl-- +GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwok-operator ./cmd/kwok-operator +cp ./bin///kwok-operator kwok-operator-- +gh -R ghrelease release upload kwok-operator-- diff --git a/test/release/testdata/build.txt b/test/release/testdata/build.txt index 032e578fd..4df37d53f 100644 --- a/test/release/testdata/build.txt +++ b/test/release/testdata/build.txt @@ -1,2 +1,3 @@ GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwok ./cmd/kwok GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwokctl ./cmd/kwokctl +GOOS= GOARCH= go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin///kwok-operator ./cmd/kwok-operator diff --git a/test/release/testdata/cross-build-with-push-bucket-staging.txt b/test/release/testdata/cross-build-with-push-bucket-staging.txt index 62fc979dc..b7ca3e534 100644 --- a/test/release/testdata/cross-build-with-push-bucket-staging.txt +++ b/test/release/testdata/cross-build-with-push-bucket-staging.txt @@ -2,23 +2,35 @@ GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Versio gsutil cp -P ./bin/linux/amd64/kwok bucket/releases/staging-prefix-/bin/linux/amd64/kwok GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/linux/amd64/kwokctl bucket/releases/staging-prefix-/bin/linux/amd64/kwokctl +GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/linux/amd64/kwok-operator bucket/releases/staging-prefix-/bin/linux/amd64/kwok-operator GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok ./cmd/kwok gsutil cp -P ./bin/linux/arm64/kwok bucket/releases/staging-prefix-/bin/linux/arm64/kwok GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/linux/arm64/kwokctl bucket/releases/staging-prefix-/bin/linux/arm64/kwokctl +GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/linux/arm64/kwok-operator bucket/releases/staging-prefix-/bin/linux/arm64/kwok-operator GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok ./cmd/kwok gsutil cp -P ./bin/darwin/amd64/kwok bucket/releases/staging-prefix-/bin/darwin/amd64/kwok GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/darwin/amd64/kwokctl bucket/releases/staging-prefix-/bin/darwin/amd64/kwokctl +GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/darwin/amd64/kwok-operator bucket/releases/staging-prefix-/bin/darwin/amd64/kwok-operator GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok ./cmd/kwok gsutil cp -P ./bin/darwin/arm64/kwok bucket/releases/staging-prefix-/bin/darwin/arm64/kwok GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/darwin/arm64/kwokctl bucket/releases/staging-prefix-/bin/darwin/arm64/kwokctl +GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/darwin/arm64/kwok-operator bucket/releases/staging-prefix-/bin/darwin/arm64/kwok-operator GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok.exe ./cmd/kwok gsutil cp -P ./bin/windows/amd64/kwok.exe bucket/releases/staging-prefix-/bin/windows/amd64/kwok.exe GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwokctl.exe ./cmd/kwokctl gsutil cp -P ./bin/windows/amd64/kwokctl.exe bucket/releases/staging-prefix-/bin/windows/amd64/kwokctl.exe +GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok-operator.exe ./cmd/kwok-operator +gsutil cp -P ./bin/windows/amd64/kwok-operator.exe bucket/releases/staging-prefix-/bin/windows/amd64/kwok-operator.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok.exe ./cmd/kwok gsutil cp -P ./bin/windows/arm64/kwok.exe bucket/releases/staging-prefix-/bin/windows/arm64/kwok.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwokctl.exe ./cmd/kwokctl gsutil cp -P ./bin/windows/arm64/kwokctl.exe bucket/releases/staging-prefix-/bin/windows/arm64/kwokctl.exe +GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok-operator.exe ./cmd/kwok-operator +gsutil cp -P ./bin/windows/arm64/kwok-operator.exe bucket/releases/staging-prefix-/bin/windows/arm64/kwok-operator.exe diff --git a/test/release/testdata/cross-build-with-push-bucket.txt b/test/release/testdata/cross-build-with-push-bucket.txt index 5a51d338d..2b4eb58f4 100644 --- a/test/release/testdata/cross-build-with-push-bucket.txt +++ b/test/release/testdata/cross-build-with-push-bucket.txt @@ -2,23 +2,35 @@ GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Versio gsutil cp -P ./bin/linux/amd64/kwok bucket/releases//bin/linux/amd64/kwok GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/linux/amd64/kwokctl bucket/releases//bin/linux/amd64/kwokctl +GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/linux/amd64/kwok-operator bucket/releases//bin/linux/amd64/kwok-operator GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok ./cmd/kwok gsutil cp -P ./bin/linux/arm64/kwok bucket/releases//bin/linux/arm64/kwok GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/linux/arm64/kwokctl bucket/releases//bin/linux/arm64/kwokctl +GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/linux/arm64/kwok-operator bucket/releases//bin/linux/arm64/kwok-operator GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok ./cmd/kwok gsutil cp -P ./bin/darwin/amd64/kwok bucket/releases//bin/darwin/amd64/kwok GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/darwin/amd64/kwokctl bucket/releases//bin/darwin/amd64/kwokctl +GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/darwin/amd64/kwok-operator bucket/releases//bin/darwin/amd64/kwok-operator GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok ./cmd/kwok gsutil cp -P ./bin/darwin/arm64/kwok bucket/releases//bin/darwin/arm64/kwok GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwokctl ./cmd/kwokctl gsutil cp -P ./bin/darwin/arm64/kwokctl bucket/releases//bin/darwin/arm64/kwokctl +GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok-operator ./cmd/kwok-operator +gsutil cp -P ./bin/darwin/arm64/kwok-operator bucket/releases//bin/darwin/arm64/kwok-operator GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok.exe ./cmd/kwok gsutil cp -P ./bin/windows/amd64/kwok.exe bucket/releases//bin/windows/amd64/kwok.exe GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwokctl.exe ./cmd/kwokctl gsutil cp -P ./bin/windows/amd64/kwokctl.exe bucket/releases//bin/windows/amd64/kwokctl.exe +GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok-operator.exe ./cmd/kwok-operator +gsutil cp -P ./bin/windows/amd64/kwok-operator.exe bucket/releases//bin/windows/amd64/kwok-operator.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok.exe ./cmd/kwok gsutil cp -P ./bin/windows/arm64/kwok.exe bucket/releases//bin/windows/arm64/kwok.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwokctl.exe ./cmd/kwokctl gsutil cp -P ./bin/windows/arm64/kwokctl.exe bucket/releases//bin/windows/arm64/kwokctl.exe +GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok-operator.exe ./cmd/kwok-operator +gsutil cp -P ./bin/windows/arm64/kwok-operator.exe bucket/releases//bin/windows/arm64/kwok-operator.exe diff --git a/test/release/testdata/cross-build-with-push-ghrelease.txt b/test/release/testdata/cross-build-with-push-ghrelease.txt index 107c86760..438849d79 100644 --- a/test/release/testdata/cross-build-with-push-ghrelease.txt +++ b/test/release/testdata/cross-build-with-push-ghrelease.txt @@ -4,33 +4,51 @@ gh -R ghrelease release upload kwok-linux-amd64 GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwokctl ./cmd/kwokctl cp ./bin/linux/amd64/kwokctl kwokctl-linux-amd64 gh -R ghrelease release upload kwokctl-linux-amd64 +GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwok-operator ./cmd/kwok-operator +cp ./bin/linux/amd64/kwok-operator kwok-operator-linux-amd64 +gh -R ghrelease release upload kwok-operator-linux-amd64 GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok ./cmd/kwok cp ./bin/linux/arm64/kwok kwok-linux-arm64 gh -R ghrelease release upload kwok-linux-arm64 GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwokctl ./cmd/kwokctl cp ./bin/linux/arm64/kwokctl kwokctl-linux-arm64 gh -R ghrelease release upload kwokctl-linux-arm64 +GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok-operator ./cmd/kwok-operator +cp ./bin/linux/arm64/kwok-operator kwok-operator-linux-arm64 +gh -R ghrelease release upload kwok-operator-linux-arm64 GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok ./cmd/kwok cp ./bin/darwin/amd64/kwok kwok-darwin-amd64 gh -R ghrelease release upload kwok-darwin-amd64 GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwokctl ./cmd/kwokctl cp ./bin/darwin/amd64/kwokctl kwokctl-darwin-amd64 gh -R ghrelease release upload kwokctl-darwin-amd64 +GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok-operator ./cmd/kwok-operator +cp ./bin/darwin/amd64/kwok-operator kwok-operator-darwin-amd64 +gh -R ghrelease release upload kwok-operator-darwin-amd64 GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok ./cmd/kwok cp ./bin/darwin/arm64/kwok kwok-darwin-arm64 gh -R ghrelease release upload kwok-darwin-arm64 GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwokctl ./cmd/kwokctl cp ./bin/darwin/arm64/kwokctl kwokctl-darwin-arm64 gh -R ghrelease release upload kwokctl-darwin-arm64 +GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok-operator ./cmd/kwok-operator +cp ./bin/darwin/arm64/kwok-operator kwok-operator-darwin-arm64 +gh -R ghrelease release upload kwok-operator-darwin-arm64 GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok.exe ./cmd/kwok cp ./bin/windows/amd64/kwok.exe kwok-windows-amd64.exe gh -R ghrelease release upload kwok-windows-amd64.exe GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwokctl.exe ./cmd/kwokctl cp ./bin/windows/amd64/kwokctl.exe kwokctl-windows-amd64.exe gh -R ghrelease release upload kwokctl-windows-amd64.exe +GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok-operator.exe ./cmd/kwok-operator +cp ./bin/windows/amd64/kwok-operator.exe kwok-operator-windows-amd64.exe +gh -R ghrelease release upload kwok-operator-windows-amd64.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok.exe ./cmd/kwok cp ./bin/windows/arm64/kwok.exe kwok-windows-arm64.exe gh -R ghrelease release upload kwok-windows-arm64.exe GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwokctl.exe ./cmd/kwokctl cp ./bin/windows/arm64/kwokctl.exe kwokctl-windows-arm64.exe gh -R ghrelease release upload kwokctl-windows-arm64.exe +GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok-operator.exe ./cmd/kwok-operator +cp ./bin/windows/arm64/kwok-operator.exe kwok-operator-windows-arm64.exe +gh -R ghrelease release upload kwok-operator-windows-arm64.exe diff --git a/test/release/testdata/cross-build.txt b/test/release/testdata/cross-build.txt index 05c3c614e..61c65c4f9 100644 --- a/test/release/testdata/cross-build.txt +++ b/test/release/testdata/cross-build.txt @@ -1,12 +1,18 @@ GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwok ./cmd/kwok GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwokctl ./cmd/kwokctl +GOOS=linux GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/amd64/kwok-operator ./cmd/kwok-operator GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok ./cmd/kwok GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwokctl ./cmd/kwokctl +GOOS=linux GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/linux/arm64/kwok-operator ./cmd/kwok-operator GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok ./cmd/kwok GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwokctl ./cmd/kwokctl +GOOS=darwin GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/amd64/kwok-operator ./cmd/kwok-operator GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok ./cmd/kwok GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwokctl ./cmd/kwokctl +GOOS=darwin GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/darwin/arm64/kwok-operator ./cmd/kwok-operator GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok.exe ./cmd/kwok GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwokctl.exe ./cmd/kwokctl +GOOS=windows GOARCH=amd64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/amd64/kwok-operator.exe ./cmd/kwok-operator GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok.exe ./cmd/kwok GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwokctl.exe ./cmd/kwokctl +GOOS=windows GOARCH=arm64 go build -ldflags '-X sigs.k8s.io/kwok/pkg/consts.Version= -X sigs.k8s.io/kwok/pkg/consts.KubeVersion=v1.31.0 -X sigs.k8s.io/kwok/pkg/consts.ImagePrefix=image-prefix' -o ./bin/windows/arm64/kwok-operator.exe ./cmd/kwok-operator