penguin/monok8s

k8s image for Mono Gateway Dev Kit

commit 6ddff7c43359418c0e3be8599468a1c19f3cfa5a724707b471bcd46c61a447bd

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-04-19T18:50:04Z
subjectDrafting ctl controller
commit 6ddff7c43359418c0e3be8599468a1c19f3cfa5a724707b471bcd46c61a447bd
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-04-19T18:50:04Z

    Drafting ctl controller
---
 README.md                                          |   8 -
 clitools/devtools/gen-admission-payload.sh         |  52 +++
 clitools/devtools/run-controller.sh                |  17 +
 clitools/go.mod                                    |  62 ++--
 clitools/go.sum                                    | 231 +++++++-----
 clitools/hack/boilerplate.go.txt                   |   1 +
 clitools/hack/tool.go                              |   9 +
 clitools/hack/update-codegen.sh                    |  36 ++
 .../pkg/apis/monok8s/v1alpha1/groupversion_info.go |  13 +-
 clitools/pkg/apis/monok8s/v1alpha1/monoksconfig.go |   1 -
 clitools/pkg/apis/monok8s/v1alpha1/osupgrade.go    | 108 ++----
 .../apis/monok8s/v1alpha1/zz_generated.deepcopy.go | 411 +++++++++++++++++++++
 clitools/pkg/cmd/agent/agent.go                    | 225 ++++++-----
 clitools/pkg/cmd/controller/controller.go          |  97 +++++
 clitools/pkg/cmd/root/root.go                      |   2 +
 clitools/pkg/controller/admission/server.go        | 157 ++++++++
 clitools/pkg/controller/osupgrade/handler.go       |  51 ++-
 clitools/pkg/controller/osupgrade/progress.go      |  27 ++
 .../pkg/generated/clientset/versioned/clientset.go | 106 ++++++
 .../versioned/fake/clientset_generated.go          |  91 +++++
 .../pkg/generated/clientset/versioned/fake/doc.go  |   6 +
 .../generated/clientset/versioned/fake/register.go |  42 +++
 .../generated/clientset/versioned/scheme/doc.go    |   6 +
 .../clientset/versioned/scheme/register.go         |  42 +++
 .../versioned/typed/monok8s/v1alpha1/doc.go        |   6 +
 .../versioned/typed/monok8s/v1alpha1/fake/doc.go   |   6 +
 .../monok8s/v1alpha1/fake/fake_monok8s_client.go   |  30 ++
 .../typed/monok8s/v1alpha1/fake/fake_osupgrade.go  |  36 ++
 .../v1alpha1/fake/fake_osupgradeprogress.go        |  38 ++
 .../typed/monok8s/v1alpha1/generated_expansion.go  |   9 +
 .../typed/monok8s/v1alpha1/monok8s_client.go       |  92 +++++
 .../versioned/typed/monok8s/v1alpha1/osupgrade.go  |  56 +++
 .../typed/monok8s/v1alpha1/osupgradeprogress.go    |  56 +++
 .../informers/externalversions/factory.go          | 249 +++++++++++++
 .../informers/externalversions/generic.go          |  50 +++
 .../internalinterfaces/factory_interfaces.go       |  26 ++
 .../externalversions/monok8s/interface.go          |  32 ++
 .../externalversions/monok8s/v1alpha1/interface.go |  38 ++
 .../externalversions/monok8s/v1alpha1/osupgrade.go |  88 +++++
 .../monok8s/v1alpha1/osupgradeprogress.go          |  88 +++++
 .../monok8s/v1alpha1/expansion_generated.go        |  21 ++
 .../listers/monok8s/v1alpha1/osupgrade.go          |  56 +++
 .../listers/monok8s/v1alpha1/osupgradeprogress.go  |  56 +++
 clitools/pkg/kube/clients.go                       |  18 +-
 clitools/pkg/node/admission.go                     | 400 ++++++++++++++++++++
 clitools/pkg/node/agent.go                         |  53 ++-
 clitools/pkg/node/kubeadm.go                       |  49 +++
 clitools/pkg/node/metadata.go                      |  14 +-
 clitools/pkg/templates/templates.go                |   3 +-
 clitools/pkg/templates/values.go                   |   7 -
 configs/cluster.env.default                        |   3 +-
 devtools/test-upgrade.sh                           |  41 ++
 52 files changed, 3084 insertions(+), 338 deletions(-)

diff --git a/README.md b/README.md
index 663fb63..3c2ef7c 100644
--- a/README.md
+++ b/README.md
@@ -86,11 +86,3 @@ make cluster-config \
 ```
 
 Check inside [configs/cluster.env.default](configs/cluster.env.default) for configuration details
-
-### Making sub stages
-```bash
-make build-base # The image that builds the kernel and everything
-make kernel     # Builds our kernel from NXP
-make initramfs
-make itb        # Builds out/board.itb (contains the kernel and the initramfs)
-```
diff --git a/clitools/devtools/gen-admission-payload.sh b/clitools/devtools/gen-admission-payload.sh
new file mode 100755
index 0000000..7b0232f
--- /dev/null
+++ b/clitools/devtools/gen-admission-payload.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+# Example: pipe this into curl
+#   ../../devtools/test-upgrade.sh \
+#   | this_script
+#   | curl -k -H 'Content-Type: application/json' --data-binary @- https://127.0.0.1:8443/admission
+
+set -euo pipefail
+
+uid=$(uuidgen 2>/dev/null)
+
+yq -o=json '.' | jq --arg uid "$uid" '
+{
+  apiVersion: "admission.k8s.io/v1",
+  kind: "AdmissionReview",
+  request: {
+    uid: $uid,
+    kind: {
+      group: "monok8s.io",
+      version: "v1alpha1",
+      kind: "OSUpgrade"
+    },
+    resource: {
+      group: "monok8s.io",
+      version: "v1alpha1",
+      resource: "osupgrades"
+    },
+    requestKind: {
+      group: "monok8s.io",
+      version: "v1alpha1",
+      kind: "OSUpgrade"
+    },
+    requestResource: {
+      group: "monok8s.io",
+      version: "v1alpha1",
+      resource: "osupgrades"
+    },
+    name: .metadata.name,
+    namespace: (.metadata.namespace // ""),
+    operation: "CREATE",
+    userInfo: {
+      username: "debug-user"
+    },
+    object: .,
+    oldObject: null,
+    dryRun: false,
+    options: {
+      apiVersion: "meta.k8s.io/v1",
+      kind: "CreateOptions"
+    }
+  }
+}'
diff --git a/clitools/devtools/run-controller.sh b/clitools/devtools/run-controller.sh
new file mode 100755
index 0000000..395b2f1
--- /dev/null
+++ b/clitools/devtools/run-controller.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
+PROJ_ROOT="$( realpath "$SCRIPT_DIR"/../ )"
+OUT_DIR="$PROJ_ROOT"/out
+
+if [ -f "$OUT_DIR/tls.key" ] && [ -f "$OUT_DIR/tls.crt" ]; then
+	echo "Use existing certs"
+else
+	echo "Generating self signed certs"
+	openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
+	  -keyout "$OUT_DIR"/tls.key -out "$OUT_DIR"/tls.crt \
+	  -subj "/CN=127.0.0.1" \
+	  -addext "subjectAltName=IP:127.0.0.1,DNS:localhost"
+fi
+
+go run "$PROJ_ROOT"/cmd/ctl controller --tls-cert-file "$OUT_DIR"/tls.crt --tls-private-key-file "$OUT_DIR"/tls.key
diff --git a/clitools/go.mod b/clitools/go.mod
index 97076d2..d3b012c 100644
--- a/clitools/go.mod
+++ b/clitools/go.mod
@@ -3,31 +3,37 @@ module example.com/monok8s
 go 1.26.0
 
 require (
+	github.com/emicklei/go-restful/v3 v3.12.2
 	github.com/klauspost/compress v1.18.5
-	github.com/spf13/cobra v1.9.1
-	golang.org/x/sys v0.31.0
+	github.com/spf13/cobra v1.10.2
+	golang.org/x/sys v0.39.0
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/api v0.34.0
-	k8s.io/apiextensions-apiserver v0.34.0
-	k8s.io/apimachinery v0.34.0
+	k8s.io/api v0.35.0
+	k8s.io/apiextensions-apiserver v0.35.0
+	k8s.io/apimachinery v0.35.0
+	k8s.io/apiserver v0.35.0
 	k8s.io/cli-runtime v0.34.0
-	k8s.io/client-go v0.34.0
+	k8s.io/client-go v0.35.0
+	k8s.io/code-generator v0.35.0
 	k8s.io/klog/v2 v2.130.1
+	sigs.k8s.io/controller-tools v0.20.1
 	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+	github.com/fatih/color v1.18.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
-	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/gobuffalo/flect v1.0.3 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/gnostic-models v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -37,30 +43,42 @@ require (
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/moby/term v0.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.66.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xlab/treeprint v1.2.0 // indirect
-	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.opentelemetry.io/otel v1.36.0 // indirect
+	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
-	golang.org/x/net v0.38.0 // indirect
-	golang.org/x/oauth2 v0.27.0 // indirect
-	golang.org/x/sync v0.12.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
-	golang.org/x/text v0.23.0 // indirect
+	golang.org/x/mod v0.31.0 // indirect
+	golang.org/x/net v0.48.0 // indirect
+	golang.org/x/oauth2 v0.30.0 // indirect
+	golang.org/x/sync v0.19.0 // indirect
+	golang.org/x/term v0.38.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
-	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	golang.org/x/tools v0.40.0 // indirect
+	google.golang.org/protobuf v1.36.8 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
-	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
-	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	k8s.io/component-base v0.35.0 // indirect
+	k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect
+	k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
+	k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
 	sigs.k8s.io/kustomize/api v0.20.1 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
 	sigs.k8s.io/randfill v1.0.0 // indirect
diff --git a/clitools/go.sum b/clitools/go.sum
index 5f0d86e..5d1b5f7 100644
--- a/clitools/go.sum
+++ b/clitools/go.sum
@@ -1,7 +1,19 @@
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -11,12 +23,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
 github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
 github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
@@ -27,29 +47,31 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
 github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
+github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
+github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
 github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
 github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
-github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
 github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -59,10 +81,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -75,25 +104,38 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
-github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
-github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
+github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
+github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
+github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
-github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -104,92 +146,115 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
 github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
-go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
-golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
 golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
 golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
-golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
+golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
+golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
+golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
+golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
+google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
+google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
-k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
-k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc=
-k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0=
-k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
-k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
+k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
+k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
+k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
+k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
+k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
+k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
 k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw=
 k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8=
-k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
-k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
+k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
+k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
+k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ=
+k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc=
+k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
+k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
+k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ=
+k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
+sigs.k8s.io/controller-tools v0.20.1 h1:gkfMt9YodI0K85oT8rVi80NTXO/kDmabKR5Ajn5GYxs=
+sigs.k8s.io/controller-tools v0.20.1/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
 sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
 sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
 sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
diff --git a/clitools/hack/boilerplate.go.txt b/clitools/hack/boilerplate.go.txt
new file mode 100644
index 0000000..c71e0e1
--- /dev/null
+++ b/clitools/hack/boilerplate.go.txt
@@ -0,0 +1 @@
+/* MIT License */
diff --git a/clitools/hack/tool.go b/clitools/hack/tool.go
new file mode 100644
index 0000000..0c55f3a
--- /dev/null
+++ b/clitools/hack/tool.go
@@ -0,0 +1,9 @@
+//go:build tools
+// +build tools
+
+package tools
+
+import (
+	_ "k8s.io/code-generator"
+	_ "sigs.k8s.io/controller-tools/cmd/controller-gen"
+)
diff --git a/clitools/hack/update-codegen.sh b/clitools/hack/update-codegen.sh
new file mode 100755
index 0000000..687a386
--- /dev/null
+++ b/clitools/hack/update-codegen.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+MODULE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+echo "MODULE ROOT: ${MODULE_ROOT}"
+
+cd "${MODULE_ROOT}"
+mkdir -p \
+	"${MODULE_ROOT}/pkg/generated/clientset" \
+	"${MODULE_ROOT}/pkg/generated/listers" \
+	"${MODULE_ROOT}/pkg/generated/informers"
+
+controller-gen \
+  object:headerFile=hack/boilerplate.go.txt \
+  paths=./pkg/apis/...
+
+MODULE="$(go list -m -f '{{.Path}}')"
+echo "MODULE: ${MODULE}"
+
+CODEGEN_PKG="$(go list -f '{{.Dir}}' -m k8s.io/code-generator)"
+echo "CODEGEN PKG: ${CODEGEN_PKG}"
+
+source "${CODEGEN_PKG}/kube_codegen.sh"
+
+mkdir -p "${MODULE_ROOT}/pkg/generated"
+
+kube::codegen::gen_helpers \
+  --boilerplate "${MODULE_ROOT}/hack/boilerplate.go.txt" \
+  "${MODULE_ROOT}/pkg/apis"
+
+kube::codegen::gen_client \
+  --with-watch \
+  --output-dir "${MODULE_ROOT}/pkg/generated" \
+  --output-pkg "${MODULE}/pkg/generated" \
+  --boilerplate "${MODULE_ROOT}/hack/boilerplate.go.txt" \
+  "${MODULE_ROOT}/pkg/apis"
diff --git a/clitools/pkg/apis/monok8s/v1alpha1/groupversion_info.go b/clitools/pkg/apis/monok8s/v1alpha1/groupversion_info.go
index 948cef2..31a05cc 100644
--- a/clitools/pkg/apis/monok8s/v1alpha1/groupversion_info.go
+++ b/clitools/pkg/apis/monok8s/v1alpha1/groupversion_info.go
@@ -14,7 +14,6 @@ var (
 	APIVersion = "monok8s.io/v1alpha1"
 
 	AltPartDeviceLink = "/dev/mksaltpart"
-	Annotation        = "monok8s.io/annotation"
 	BootStateFile     = "/run/monok8s/boot-state.env"
 	CatalogURL        = "https://example.com/monok8s.io/v1alpha1/catalog.yaml"
 	ControlAgentKey   = "monok8s.io/control-agent"
@@ -25,18 +24,20 @@ var (
 )
 
 var (
-	GroupVersion  = schema.GroupVersion{Group: Group, Version: Version}
-	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
-	AddToScheme   = SchemeBuilder.AddToScheme
+	SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}
+	SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
+	AddToScheme        = SchemeBuilder.AddToScheme
 )
 
 func addKnownTypes(scheme *runtime.Scheme) error {
-	scheme.AddKnownTypes(GroupVersion,
+	scheme.AddKnownTypes(SchemeGroupVersion,
 		&MonoKSConfig{},
 		&MonoKSConfigList{},
 		&OSUpgrade{},
 		&OSUpgradeList{},
+		&OSUpgradeProgress{},
+		&OSUpgradeProgressList{},
 	)
-	metav1.AddToGroupVersion(scheme, GroupVersion)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
 	return nil
 }
diff --git a/clitools/pkg/apis/monok8s/v1alpha1/monoksconfig.go b/clitools/pkg/apis/monok8s/v1alpha1/monoksconfig.go
index 758af8e..e9d330d 100644
--- a/clitools/pkg/apis/monok8s/v1alpha1/monoksconfig.go
+++ b/clitools/pkg/apis/monok8s/v1alpha1/monoksconfig.go
@@ -40,7 +40,6 @@ type MonoKSConfigSpec struct {
 	KubeProxyNodePortAddresses    []string          `json:"kubeProxyNodePortAddresses,omitempty" yaml:"kubeProxyNodePortAddresses,omitempty"`
 	SubjectAltNames               []string          `json:"subjectAltNames,omitempty" yaml:"subjectAltNames,omitempty"`
 	NodeLabels                    map[string]string `json:"nodeLabels,omitempty" yaml:"nodeLabels,omitempty"`
-	NodeAnnotations               map[string]string `json:"nodeAnnotations,omitempty" yaml:"nodeAnnotations,omitempty"`
 	Network                       NetworkSpec       `json:"network,omitempty" yaml:"network,omitempty"`
 }
 
diff --git a/clitools/pkg/apis/monok8s/v1alpha1/osupgrade.go b/clitools/pkg/apis/monok8s/v1alpha1/osupgrade.go
index fee1d86..a0f8881 100644
--- a/clitools/pkg/apis/monok8s/v1alpha1/osupgrade.go
+++ b/clitools/pkg/apis/monok8s/v1alpha1/osupgrade.go
@@ -2,7 +2,6 @@ package v1alpha1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 )
 
 type OSUpgradePhase string
@@ -28,6 +27,7 @@ const (
 	OSUpgradeProgressPhaseRejected    OSUpgradeProgressPhase = "rejected"
 )
 
+// +genclient
 // +kubebuilder:object:root=true
 // +kubebuilder:subresource:status
 // +kubebuilder:resource:scope=Namespaced,shortName=osu
@@ -52,46 +52,22 @@ type OSUpgradeList struct {
 	Items           []OSUpgrade `json:"items" yaml:"items"`
 }
 
-// OSUpgradeSpec defines the desired state of an OSUpgrade.
 type OSUpgradeSpec struct {
-	// DesiredVersion is the requested target version.
-	//
-	// It may be either:
-	// - "stable" to use the catalog's current stable version
-	// - an explicit version such as "v1.35.3"
-	//
-	// The resolved target version is reported in status.
 	// +kubebuilder:validation:MinLength=1
 	DesiredVersion string `json:"desiredVersion,omitempty" yaml:"desiredVersion,omitempty"`
 
-	// FlashProfile controls how aggressively the image is written to disk.
-	//
-	// Supported values are:
-	// - "fast": prioritize speed
-	// - "balanced": default tradeoff between speed and system impact
-	// - "safe": minimize I/O pressure on the node
-	//
-	// If unset, the controller should use a default profile.
 	// +kubebuilder:validation:Enum=fast;balanced;safe
 	// +kubebuilder:default=balanced
-	FlashProfile string `json:"flashProfile,omitempty"`
+	FlashProfile string `json:"flashProfile,omitempty" yaml:"flashProfile,omitempty"`
 
-	// Catalog specifies how available versions and images are resolved.
-	//
-	// This may point to a remote catalog, inline catalog data, or another source,
-	// depending on the VersionCatalogSource definition.
-	Catalog *VersionCatalogSource `json:"catalog,omitempty"`
-
-	// NodeSelector limits the upgrade to nodes whose labels match this selector.
-	//
-	// If unset, the upgrade applies to all eligible nodes.
+	Catalog      *VersionCatalogSource `json:"catalog,omitempty" yaml:"catalog,omitempty"`
 	NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty" yaml:"nodeSelector,omitempty"`
 }
 
 type VersionCatalogSource struct {
-	URL       string `json:"url,omitempty"`
-	Inline    string `json:"inline,omitempty"`
-	ConfigMap string `json:"configMapRef,omitempty"`
+	URL       string `json:"url,omitempty" yaml:"url,omitempty"`
+	Inline    string `json:"inline,omitempty" yaml:"inline,omitempty"`
+	ConfigMap string `json:"configMapRef,omitempty" yaml:"configMapRef,omitempty"`
 }
 
 type OSUpgradeStatus struct {
@@ -100,10 +76,8 @@ type OSUpgradeStatus struct {
 	ObservedGeneration int64              `json:"observedGeneration,omitempty" yaml:"observedGeneration,omitempty"`
 	Summary            OSUpgradeSummary   `json:"summary,omitempty" yaml:"summary,omitempty"`
 	Conditions         []metav1.Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
-
-	// Optional, useful when rejected.
-	Reason  string `json:"reason,omitempty" yaml:"reason,omitempty"`
-	Message string `json:"message,omitempty" yaml:"message,omitempty"`
+	Reason             string             `json:"reason,omitempty" yaml:"reason,omitempty"`
+	Message            string             `json:"message,omitempty" yaml:"message,omitempty"`
 }
 
 type OSUpgradeSummary struct {
@@ -114,6 +88,7 @@ type OSUpgradeSummary struct {
 	FailedNodes    int32 `json:"failedNodes,omitempty" yaml:"failedNodes,omitempty"`
 }
 
+// +genclient
 // +kubebuilder:object:root=true
 // +kubebuilder:subresource:status
 // +kubebuilder:resource:scope=Namespaced,shortName=osup
@@ -143,56 +118,23 @@ type OSUpgradeProgressSpec struct {
 }
 
 type OSUpgradeSourceRef struct {
-	Name string `json:"name,omitempty" yaml:"name,omitempty"`
+	Name      string `json:"name,omitempty" yaml:"name,omitempty"`
+	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
 }
 
 type OSUpgradeProgressStatus struct {
-	CurrentVersion string                 `json:"currentVersion,omitempty" yaml:"currentVersion,omitempty"`
-	TargetVersion  string                 `json:"targetVersion,omitempty" yaml:"targetVersion,omitempty"`
-	Phase          OSUpgradeProgressPhase `json:"phase,omitempty" yaml:"phase,omitempty"`
-
-	StartedAt         *metav1.Time `json:"startedAt,omitempty" yaml:"startedAt,omitempty"`
-	CompletedAt       *metav1.Time `json:"completedAt,omitempty" yaml:"completedAt,omitempty"`
-	LastUpdatedAt     *metav1.Time `json:"lastUpdatedAt,omitempty" yaml:"lastUpdatedAt,omitempty"`
-	RetryCount        int32        `json:"retryCount,omitempty" yaml:"retryCount,omitempty"`
-	InactivePartition string       `json:"inactivePartition,omitempty" yaml:"inactivePartition,omitempty"`
-	FailureReason     string       `json:"failureReason,omitempty" yaml:"failureReason,omitempty"`
-	Message           string       `json:"message,omitempty" yaml:"message,omitempty"`
-
-	PlannedPath []string `json:"plannedPath,omitempty"`
-	CurrentStep int32    `json:"currentStep,omitempty"`
-	CurrentFrom string   `json:"currentFrom,omitempty"`
-	CurrentTo   string   `json:"currentTo,omitempty"`
-}
-
-func (in *OSUpgrade) DeepCopyObject() runtime.Object {
-	if in == nil {
-		return nil
-	}
-	out := *in
-	return &out
-}
-
-func (in *OSUpgradeList) DeepCopyObject() runtime.Object {
-	if in == nil {
-		return nil
-	}
-	out := *in
-	return &out
-}
-
-func (in *OSUpgradeProgress) DeepCopyObject() runtime.Object {
-	if in == nil {
-		return nil
-	}
-	out := *in
-	return &out
-}
-
-func (in *OSUpgradeProgressList) DeepCopyObject() runtime.Object {
-	if in == nil {
-		return nil
-	}
-	out := *in
-	return &out
+	CurrentVersion    string                 `json:"currentVersion,omitempty" yaml:"currentVersion,omitempty"`
+	TargetVersion     string                 `json:"targetVersion,omitempty" yaml:"targetVersion,omitempty"`
+	Phase             OSUpgradeProgressPhase `json:"phase,omitempty" yaml:"phase,omitempty"`
+	StartedAt         *metav1.Time           `json:"startedAt,omitempty" yaml:"startedAt,omitempty"`
+	CompletedAt       *metav1.Time           `json:"completedAt,omitempty" yaml:"completedAt,omitempty"`
+	LastUpdatedAt     *metav1.Time           `json:"lastUpdatedAt,omitempty" yaml:"lastUpdatedAt,omitempty"`
+	RetryCount        int32                  `json:"retryCount,omitempty" yaml:"retryCount,omitempty"`
+	InactivePartition string                 `json:"inactivePartition,omitempty" yaml:"inactivePartition,omitempty"`
+	FailureReason     string                 `json:"failureReason,omitempty" yaml:"failureReason,omitempty"`
+	Message           string                 `json:"message,omitempty" yaml:"message,omitempty"`
+	PlannedPath       []string               `json:"plannedPath,omitempty" yaml:"plannedPath,omitempty"`
+	CurrentStep       int32                  `json:"currentStep,omitempty" yaml:"currentStep,omitempty"`
+	CurrentFrom       string                 `json:"currentFrom,omitempty" yaml:"currentFrom,omitempty"`
+	CurrentTo         string                 `json:"currentTo,omitempty" yaml:"currentTo,omitempty"`
 }
diff --git a/clitools/pkg/apis/monok8s/v1alpha1/zz_generated.deepcopy.go b/clitools/pkg/apis/monok8s/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 0000000..df833d7
--- /dev/null
+++ b/clitools/pkg/apis/monok8s/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,411 @@
+//go:build !ignore_autogenerated
+
+/* MIT License */
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MonoKSConfig) DeepCopyInto(out *MonoKSConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	if in.Status != nil {
+		in, out := &in.Status, &out.Status
+		*out = new(MonoKSConfigStatus)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonoKSConfig.
+func (in *MonoKSConfig) DeepCopy() *MonoKSConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(MonoKSConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MonoKSConfigList) DeepCopyInto(out *MonoKSConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]MonoKSConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonoKSConfigList.
+func (in *MonoKSConfigList) DeepCopy() *MonoKSConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(MonoKSConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MonoKSConfigSpec) DeepCopyInto(out *MonoKSConfigSpec) {
+	*out = *in
+	if in.KubeProxyNodePortAddresses != nil {
+		in, out := &in.KubeProxyNodePortAddresses, &out.KubeProxyNodePortAddresses
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.SubjectAltNames != nil {
+		in, out := &in.SubjectAltNames, &out.SubjectAltNames
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.NodeLabels != nil {
+		in, out := &in.NodeLabels, &out.NodeLabels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	in.Network.DeepCopyInto(&out.Network)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonoKSConfigSpec.
+func (in *MonoKSConfigSpec) DeepCopy() *MonoKSConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(MonoKSConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MonoKSConfigStatus) DeepCopyInto(out *MonoKSConfigStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.AppliedSteps != nil {
+		in, out := &in.AppliedSteps, &out.AppliedSteps
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonoKSConfigStatus.
+func (in *MonoKSConfigStatus) DeepCopy() *MonoKSConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(MonoKSConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) {
+	*out = *in
+	if in.DNSNameservers != nil {
+		in, out := &in.DNSNameservers, &out.DNSNameservers
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.DNSSearchDomains != nil {
+		in, out := &in.DNSSearchDomains, &out.DNSSearchDomains
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkSpec.
+func (in *NetworkSpec) DeepCopy() *NetworkSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(NetworkSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgrade) DeepCopyInto(out *OSUpgrade) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	if in.Status != nil {
+		in, out := &in.Status, &out.Status
+		*out = new(OSUpgradeStatus)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgrade.
+func (in *OSUpgrade) DeepCopy() *OSUpgrade {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgrade)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *OSUpgrade) 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 *OSUpgradeList) DeepCopyInto(out *OSUpgradeList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]OSUpgrade, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeList.
+func (in *OSUpgradeList) DeepCopy() *OSUpgradeList {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *OSUpgradeList) 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 *OSUpgradeProgress) DeepCopyInto(out *OSUpgradeProgress) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	if in.Status != nil {
+		in, out := &in.Status, &out.Status
+		*out = new(OSUpgradeProgressStatus)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeProgress.
+func (in *OSUpgradeProgress) DeepCopy() *OSUpgradeProgress {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeProgress)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *OSUpgradeProgress) 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 *OSUpgradeProgressList) DeepCopyInto(out *OSUpgradeProgressList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]OSUpgradeProgress, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeProgressList.
+func (in *OSUpgradeProgressList) DeepCopy() *OSUpgradeProgressList {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeProgressList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *OSUpgradeProgressList) 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 *OSUpgradeProgressSpec) DeepCopyInto(out *OSUpgradeProgressSpec) {
+	*out = *in
+	out.SourceRef = in.SourceRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeProgressSpec.
+func (in *OSUpgradeProgressSpec) DeepCopy() *OSUpgradeProgressSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeProgressSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgradeProgressStatus) DeepCopyInto(out *OSUpgradeProgressStatus) {
+	*out = *in
+	if in.StartedAt != nil {
+		in, out := &in.StartedAt, &out.StartedAt
+		*out = (*in).DeepCopy()
+	}
+	if in.CompletedAt != nil {
+		in, out := &in.CompletedAt, &out.CompletedAt
+		*out = (*in).DeepCopy()
+	}
+	if in.LastUpdatedAt != nil {
+		in, out := &in.LastUpdatedAt, &out.LastUpdatedAt
+		*out = (*in).DeepCopy()
+	}
+	if in.PlannedPath != nil {
+		in, out := &in.PlannedPath, &out.PlannedPath
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeProgressStatus.
+func (in *OSUpgradeProgressStatus) DeepCopy() *OSUpgradeProgressStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeProgressStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgradeSourceRef) DeepCopyInto(out *OSUpgradeSourceRef) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeSourceRef.
+func (in *OSUpgradeSourceRef) DeepCopy() *OSUpgradeSourceRef {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeSourceRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgradeSpec) DeepCopyInto(out *OSUpgradeSpec) {
+	*out = *in
+	if in.Catalog != nil {
+		in, out := &in.Catalog, &out.Catalog
+		*out = new(VersionCatalogSource)
+		**out = **in
+	}
+	if in.NodeSelector != nil {
+		in, out := &in.NodeSelector, &out.NodeSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeSpec.
+func (in *OSUpgradeSpec) DeepCopy() *OSUpgradeSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgradeStatus) DeepCopyInto(out *OSUpgradeStatus) {
+	*out = *in
+	out.Summary = in.Summary
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeStatus.
+func (in *OSUpgradeStatus) DeepCopy() *OSUpgradeStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OSUpgradeSummary) DeepCopyInto(out *OSUpgradeSummary) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSUpgradeSummary.
+func (in *OSUpgradeSummary) DeepCopy() *OSUpgradeSummary {
+	if in == nil {
+		return nil
+	}
+	out := new(OSUpgradeSummary)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VersionCatalogSource) DeepCopyInto(out *VersionCatalogSource) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionCatalogSource.
+func (in *VersionCatalogSource) DeepCopy() *VersionCatalogSource {
+	if in == nil {
+		return nil
+	}
+	out := new(VersionCatalogSource)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/clitools/pkg/cmd/agent/agent.go b/clitools/pkg/cmd/agent/agent.go
index 47a2f4b..dfc8148 100644
--- a/clitools/pkg/cmd/agent/agent.go
+++ b/clitools/pkg/cmd/agent/agent.go
@@ -9,10 +9,7 @@ import (
 
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/klog/v2"
 
@@ -23,18 +20,13 @@ import (
 	"example.com/monok8s/pkg/templates"
 )
 
-const defaultPollInterval = 15 * time.Second
-
-var runtimeDefaultUnstructuredConverter = runtime.DefaultUnstructuredConverter
-
 func NewCmdAgent(flags *genericclioptions.ConfigFlags) *cobra.Command {
 	var namespace string
 	var envFile string
-	var pollInterval time.Duration
 
 	cmd := &cobra.Command{
 		Use:   "agent --env-file path",
-		Short: "Watch OSUpgrade resources and process matching upgrades for this node",
+		Short: "Watch OSUpgradeProgress resources for this node and process upgrades",
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			if envFile == "" {
 				return fmt.Errorf("--env-file is required")
@@ -61,7 +53,6 @@ func NewCmdAgent(flags *genericclioptions.ConfigFlags) *cobra.Command {
 				"node", cfg.Spec.NodeName,
 				"namespace", namespace,
 				"envFile", envFile,
-				"pollInterval", pollInterval,
 			)
 
 			clients, err := kube.NewClients(flags)
@@ -69,13 +60,12 @@ func NewCmdAgent(flags *genericclioptions.ConfigFlags) *cobra.Command {
 				return fmt.Errorf("create kube clients: %w", err)
 			}
 
-			return runPollLoop(ctx, clients, namespace, cfg.Spec.NodeName, pollInterval)
+			return runWatchLoop(ctx, clients, namespace, cfg.Spec.NodeName)
 		},
 	}
 
-	cmd.Flags().StringVar(&namespace, "namespace", "kube-system", "namespace to watch")
+	cmd.Flags().StringVar(&namespace, "namespace", templates.DefaultNamespace, "namespace to watch")
 	cmd.Flags().StringVar(&envFile, "env-file", "", "path to env file containing MKS_* variables")
-	cmd.Flags().DurationVar(&pollInterval, "poll-interval", defaultPollInterval, "poll interval for OSUpgrade resources")
 
 	return cmd
 }
@@ -94,9 +84,9 @@ func waitForControlGate(ctx context.Context, envFile string, pollInterval time.D
 	for {
 		_, err := os.Stat(marker)
 		if err == nil {
-			klog.InfoS("Control gate is present; waiting before starting poll loop", "path", marker)
+			klog.InfoS("Control gate is present; waiting before starting watch loop", "path", marker)
 		} else if os.IsNotExist(err) {
-			klog.InfoS("Control gate not present; starting poll loop", "path", marker)
+			klog.InfoS("Control gate not present; starting watch loop", "path", marker)
 			return nil
 		} else {
 			return fmt.Errorf("stat upgrade marker %s: %w", marker, err)
@@ -110,126 +100,181 @@ func waitForControlGate(ctx context.Context, envFile string, pollInterval time.D
 	}
 }
 
-func runPollLoop(ctx context.Context, clients *kube.Clients, namespace, nodeName string, interval time.Duration) error {
-	gvr := schema.GroupVersionResource{
-		Group:    monov1alpha1.Group,
-		Version:  monov1alpha1.Version,
-		Resource: "osupgrades",
-	}
-
-	ticker := time.NewTicker(interval)
-	defer ticker.Stop()
+func runWatchLoop(ctx context.Context, clients *kube.Clients, namespace, nodeName string) error {
+	var resourceVersion string
 
 	for {
-		if err := pollOnce(ctx, clients, gvr, namespace, nodeName); err != nil {
-			klog.ErrorS(err, "poll failed", "namespace", namespace, "node", nodeName)
+		if ctx.Err() != nil {
+			return ctx.Err()
 		}
 
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
+		err := watchOnce(ctx, clients, namespace, nodeName, &resourceVersion)
+		if err != nil {
+			if ctx.Err() != nil {
+				return ctx.Err()
+			}
+			klog.ErrorS(err, "watch failed; retrying",
+				"namespace", namespace,
+				"node", nodeName,
+				"resourceVersion", resourceVersion,
+			)
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			case <-time.After(2 * time.Second):
+			}
+			continue
 		}
 	}
 }
 
-func pollOnce(
+func watchOnce(
 	ctx context.Context,
 	clients *kube.Clients,
-	gvr schema.GroupVersionResource,
 	namespace string,
 	nodeName string,
+	resourceVersion *string,
 ) error {
-	list, err := clients.Dynamic.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
-	if err != nil {
-		return fmt.Errorf("list osupgrades: %w", err)
-	}
-
-	klog.InfoS("agent tick", "namespace", namespace, "items", len(list.Items), "node", nodeName)
 
-	nodeLabels := labels.Set{
-		"kubernetes.io/hostname":   nodeName,
-		"monok8s.io/node-name":     nodeName,
-		"monok8s.io/control-agent": "true",
+	list, err := clients.MonoKS.
+		Monok8sV1alpha1().
+		OSUpgradeProgresses(namespace).
+		List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return fmt.Errorf("list osupgradeprogresses: %w", err)
 	}
 
 	for i := range list.Items {
 		item := &list.Items[i]
-
-		osu, err := decodeOSUpgrade(item)
-		if err != nil {
-			klog.ErrorS(err, "failed to decode osupgrade",
-				"name", item.GetName(),
-				"resourceVersion", item.GetResourceVersion(),
-			)
+		if !targetsNode(item, nodeName) {
 			continue
 		}
-
-		if !matchesNode(osu, nodeName, nodeLabels) {
-			klog.V(2).InfoS("skipping osupgrade; not targeted to this node",
-				"name", osu.Name,
-				"node", nodeName,
-			)
+		if !shouldHandle(item) {
 			continue
 		}
 
-		klog.InfoS("matched osupgrade",
-			"name", osu.Name,
+		klog.InfoS("found existing osupgradeprogress",
+			"name", item.Name,
 			"node", nodeName,
-			"desiredVersion", osu.Spec.DesiredVersion,
-			"phase", statusPhase(osu.Status),
-			"resourceVersion", osu.ResourceVersion,
+			"phase", progressPhase(item.Status),
+			"resourceVersion", item.ResourceVersion,
 		)
 
-		if err := osupgradeController.HandleOSUpgrade(ctx, clients, namespace, nodeName, osu); err != nil {
-			klog.ErrorS(err, "failed to handle osupgrade",
-				"name", osu.Name,
+		if err := osupgradeController.HandleOSUpgradeProgress(ctx, clients, namespace, nodeName, item); err != nil {
+			klog.ErrorS(err, "failed to handle existing osupgradeprogress",
+				"name", item.Name,
 				"node", nodeName,
 			)
-			continue
 		}
 	}
 
-	return nil
-}
+	*resourceVersion = list.ResourceVersion
 
-func decodeOSUpgrade(item *unstructured.Unstructured) (*monov1alpha1.OSUpgrade, error) {
-	var osu monov1alpha1.OSUpgrade
-	if err := runtimeDefaultUnstructuredConverter.FromUnstructured(item.Object, &osu); err != nil {
-		return nil, fmt.Errorf("convert unstructured to OSUpgrade: %w", err)
+	w, err := clients.MonoKS.
+		Monok8sV1alpha1().
+		OSUpgradeProgresses(namespace).
+		Watch(ctx, metav1.ListOptions{
+			ResourceVersion: *resourceVersion,
+		})
+	if err != nil {
+		return fmt.Errorf("watch osupgradeprogresses: %w", err)
 	}
-	return &osu, nil
-}
+	defer w.Stop()
 
-func matchesNode(osu *monov1alpha1.OSUpgrade, nodeName string, nodeLabels labels.Set) bool {
-	if osu == nil {
-		return false
-	}
+	klog.InfoS("watching osupgradeprogresses",
+		"namespace", namespace,
+		"node", nodeName,
+		"resourceVersion", *resourceVersion,
+	)
 
-	sel := osu.Spec.NodeSelector
-	if sel == nil {
-		// No selector means "match all nodes".
-		return true
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+
+		case evt, ok := <-w.ResultChan():
+			if !ok {
+				return fmt.Errorf("watch channel closed")
+			}
+
+			switch evt.Type {
+			case watch.Bookmark:
+				obj, ok := evt.Object.(*monov1alpha1.OSUpgradeProgress)
+				if ok && obj != nil && obj.ResourceVersion != "" {
+					*resourceVersion = obj.ResourceVersion
+				}
+				continue
+
+			case watch.Error:
+				return fmt.Errorf("watch returned error event")
+			}
+
+			osup, ok := evt.Object.(*monov1alpha1.OSUpgradeProgress)
+			if !ok {
+				klog.V(1).InfoS("skipping unexpected watch object type",
+					"type", fmt.Sprintf("%T", evt.Object),
+				)
+				continue
+			}
+
+			if osup.ResourceVersion != "" {
+				*resourceVersion = osup.ResourceVersion
+			}
+
+			if !targetsNode(osup, nodeName) {
+				continue
+			}
+			if !shouldHandle(osup) {
+				klog.V(2).InfoS("skipping osupgradeprogress due to phase",
+					"name", osup.Name,
+					"node", nodeName,
+					"phase", progressPhase(osup.Status),
+					"eventType", evt.Type,
+				)
+				continue
+			}
+
+			klog.InfoS("received osupgradeprogress event",
+				"name", osup.Name,
+				"node", nodeName,
+				"phase", progressPhase(osup.Status),
+				"eventType", evt.Type,
+				"resourceVersion", osup.ResourceVersion,
+			)
+
+			if err := osupgradeController.HandleOSUpgradeProgress(ctx, clients, namespace, nodeName, osup); err != nil {
+				klog.ErrorS(err, "failed to handle osupgradeprogress",
+					"name", osup.Name,
+					"node", nodeName,
+					"eventType", evt.Type,
+				)
+			}
+		}
 	}
+}
 
-	selector, err := metav1.LabelSelectorAsSelector(sel)
-	if err != nil {
-		klog.ErrorS(err, "invalid node selector on osupgrade", "name", osu.Name)
+func targetsNode(osup *monov1alpha1.OSUpgradeProgress, nodeName string) bool {
+	if osup == nil {
 		return false
 	}
+	return osup.Spec.NodeName == nodeName
+}
 
-	if selector.Empty() {
-		return true
+func shouldHandle(osup *monov1alpha1.OSUpgradeProgress) bool {
+	if osup == nil {
+		return false
 	}
 
-	if selector.Matches(nodeLabels) {
+	switch osup.Status.Phase {
+	case "",
+		monov1alpha1.OSUpgradeProgressPhasePending:
 		return true
+	default:
+		return false
 	}
-
-	return false
 }
 
-func statusPhase(st *monov1alpha1.OSUpgradeStatus) string {
+func progressPhase(st *monov1alpha1.OSUpgradeProgressStatus) string {
 	if st == nil {
 		return ""
 	}
diff --git a/clitools/pkg/cmd/controller/controller.go b/clitools/pkg/cmd/controller/controller.go
new file mode 100644
index 0000000..88b57d6
--- /dev/null
+++ b/clitools/pkg/cmd/controller/controller.go
@@ -0,0 +1,97 @@
+package controller
+
+import (
+	"context"
+	"net"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/spf13/cobra"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"k8s.io/klog/v2"
+
+	mksadmission "example.com/monok8s/pkg/controller/admission"
+	"example.com/monok8s/pkg/kube"
+	"example.com/monok8s/pkg/templates"
+)
+
+type ServerConfig struct {
+	TLSCertFile       string `json:"tlsCertFile,omitempty"`
+	TLSPrivateKeyFile string `json:"tlsPrivateKeyFile,omitempty"`
+}
+
+func NewCmdController(flags *genericclioptions.ConfigFlags) *cobra.Command {
+	var namespace string = templates.DefaultNamespace
+	var conf ServerConfig
+
+	cmd := &cobra.Command{
+		Use:   "controller",
+		Short: "Admission controller that handles OSUpgrade resources",
+		RunE: func(cmd *cobra.Command, _ []string) error {
+
+			ctx := cmd.Context()
+
+			klog.InfoS("starting controller",
+				"namespace", namespace,
+			)
+
+			clients, err := kube.NewClients(flags)
+			if err != nil {
+				return err
+			}
+
+			return httpListen(ctx, clients, conf)
+		},
+	}
+
+	cmd.Flags().StringVar(&namespace, "namespace", templates.DefaultNamespace, "namespace to watch")
+	cmd.Flags().StringVar(&conf.TLSCertFile, "tls-cert-file", conf.TLSCertFile,
+		"File containing x509 Certificate used for serving HTTPS (with intermediate certs, if any, concatenated after server cert).")
+	cmd.Flags().StringVar(&conf.TLSPrivateKeyFile, "tls-private-key-file", conf.TLSPrivateKeyFile, "File containing x509 private key matching --tls-cert-file.")
+
+	return cmd
+}
+
+func httpListen(ctx context.Context, clients *kube.Clients, conf ServerConfig) error {
+	address, port := "", "8443"
+	addr := net.JoinHostPort(address, port)
+
+	ns := os.Getenv("POD_NAMESPACE")
+	nodeName := os.Getenv("NODE_NAME")
+
+	server := mksadmission.NewServer(ctx, clients, ns, nodeName)
+
+	s := &http.Server{
+		Addr:           addr,
+		Handler:        server,
+		IdleTimeout:    90 * time.Second,
+		ReadTimeout:    4 * 60 * time.Minute,
+		WriteTimeout:   4 * 60 * time.Minute,
+		MaxHeaderBytes: 1 << 20,
+	}
+
+	if conf.TLSCertFile != "" {
+		klog.InfoS("starting HTTPS server",
+			"addr", addr,
+			"certFile", conf.TLSCertFile,
+			"keyFile", conf.TLSPrivateKeyFile,
+		)
+
+		if err := s.ListenAndServeTLS(conf.TLSCertFile, conf.TLSPrivateKeyFile); err != nil {
+			klog.ErrorS(err, "HTTPS server failed")
+			os.Exit(1)
+		}
+	} else {
+		klog.InfoS("starting HTTP server",
+			"addr", addr,
+		)
+
+		if err := s.ListenAndServe(); err != nil {
+			klog.ErrorS(err, "HTTP server failed")
+			os.Exit(1)
+		}
+	}
+
+	return nil
+}
diff --git a/clitools/pkg/cmd/root/root.go b/clitools/pkg/cmd/root/root.go
index 30d5c29..2b82b34 100644
--- a/clitools/pkg/cmd/root/root.go
+++ b/clitools/pkg/cmd/root/root.go
@@ -6,6 +6,7 @@ import (
 
 	agentcmd "example.com/monok8s/pkg/cmd/agent"
 	checkconfigcmd "example.com/monok8s/pkg/cmd/checkconfig"
+	controllercmd "example.com/monok8s/pkg/cmd/controller"
 	createcmd "example.com/monok8s/pkg/cmd/create"
 	initcmd "example.com/monok8s/pkg/cmd/initcmd"
 	internalcmd "example.com/monok8s/pkg/cmd/internal"
@@ -45,6 +46,7 @@ func NewRootCmd() *cobra.Command {
 		checkconfigcmd.NewCmdCheckConfig(),
 		createcmd.NewCmdCreate(),
 		agentcmd.NewCmdAgent(flags),
+		controllercmd.NewCmdController(flags),
 		internalcmd.NewCmdInternal(),
 	)
 	return cmd
diff --git a/clitools/pkg/controller/admission/server.go b/clitools/pkg/controller/admission/server.go
new file mode 100644
index 0000000..84ab259
--- /dev/null
+++ b/clitools/pkg/controller/admission/server.go
@@ -0,0 +1,157 @@
+package admission
+
+import (
+	"context"
+	"io"
+	"net/http"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	"example.com/monok8s/pkg/controller/osupgrade"
+	"example.com/monok8s/pkg/kube"
+
+	"github.com/emicklei/go-restful/v3"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/apiserver/pkg/server/httplog"
+	"k8s.io/klog/v2"
+)
+
+var (
+	scheme       = runtime.NewScheme()
+	codecs       = serializer.NewCodecFactory(scheme)
+	deserializer = codecs.UniversalDeserializer()
+)
+
+var statusesNoTracePred = httplog.StatusIsNot(
+	http.StatusOK,
+	http.StatusFound,
+	http.StatusMovedPermanently,
+	http.StatusTemporaryRedirect,
+	http.StatusBadRequest,
+	http.StatusNotFound,
+	http.StatusSwitchingProtocols,
+)
+
+func init() {
+	_ = admissionv1.AddToScheme(scheme)
+	_ = monov1alpha1.AddToScheme(scheme)
+}
+
+type Server struct {
+	restfulCont *restful.Container
+
+	ctx       context.Context
+	clients   *kube.Clients
+	namespace string
+	nodeName  string
+}
+
+func NewServer(ctx context.Context, clients *kube.Clients, namespace, nodeName string) *Server {
+	s := &Server{
+		ctx:       ctx,
+		clients:   clients,
+		namespace: namespace,
+		nodeName:  nodeName,
+	}
+	s.Initialize()
+	return s
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	if s == nil {
+		http.Error(w, "admission server is nil", http.StatusInternalServerError)
+		return
+	}
+	if s.restfulCont == nil {
+		http.Error(w, "admission server not initialized", http.StatusInternalServerError)
+		return
+	}
+
+	handler := httplog.WithLogging(s.restfulCont, statusesNoTracePred)
+	handler.ServeHTTP(w, req)
+}
+
+func (s *Server) Initialize() {
+	s.restfulCont = restful.NewContainer()
+
+	ws := new(restful.WebService)
+
+	ws.Path("/admission").
+		Consumes(restful.MIME_JSON).
+		Produces(restful.MIME_JSON)
+
+	ws.Route(ws.POST("").To(s.triggerAdmission).
+		Reads(admissionv1.AdmissionReview{}).
+		Writes(admissionv1.AdmissionReview{}))
+
+	s.restfulCont.Add(ws)
+}
+
+func (s *Server) triggerAdmission(request *restful.Request, response *restful.Response) {
+	body, err := io.ReadAll(request.Request.Body)
+	if err != nil {
+		_ = response.WriteError(http.StatusBadRequest, err)
+		return
+	}
+
+	var reviewReq admissionv1.AdmissionReview
+	if _, _, err := deserializer.Decode(body, nil, &reviewReq); err != nil {
+		_ = response.WriteError(http.StatusBadRequest, err)
+		return
+	}
+	if reviewReq.Request == nil {
+		_ = response.WriteErrorString(http.StatusBadRequest, "missing admission request")
+		return
+	}
+
+	resp := admissionv1.AdmissionReview{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "AdmissionReview",
+			APIVersion: "admission.k8s.io/v1",
+		},
+		Response: &admissionv1.AdmissionResponse{
+			UID:     reviewReq.Request.UID,
+			Allowed: true,
+			Result:  &metav1.Status{Message: "OK"},
+		},
+	}
+
+	var osu monov1alpha1.OSUpgrade
+	if _, _, err := deserializer.Decode(reviewReq.Request.Object.Raw, nil, &osu); err != nil {
+		klog.V(1).InfoS("Skipping non-OSUpgrade resource",
+			"uid", reviewReq.Request.UID,
+			"kind", reviewReq.Request.Kind.Kind,
+			"operation", reviewReq.Request.Operation,
+			"err", err,
+		)
+		_ = response.WriteEntity(resp)
+		return
+	}
+
+	klog.InfoS("Received OSUpgrade admission",
+		"uid", reviewReq.Request.UID,
+		"operation", reviewReq.Request.Operation,
+		"name", osu.Name,
+		"namespace", osu.Namespace,
+		"node", s.nodeName,
+	)
+
+	// Resolve every node name
+	if err := osupgrade.EnsureOSUpgradeProgressForNode(
+		s.ctx,
+		s.clients,
+		s.namespace,
+		s.nodeName,
+		&osu,
+	); err != nil {
+		klog.ErrorS(err, "ensure OSUpgradeProgress for node failed",
+			"osupgrade", osu.Name,
+			"node", s.nodeName,
+		)
+	}
+
+	_ = response.WriteEntity(resp)
+}
diff --git a/clitools/pkg/controller/osupgrade/handler.go b/clitools/pkg/controller/osupgrade/handler.go
index 4bd6e24..af60af6 100644
--- a/clitools/pkg/controller/osupgrade/handler.go
+++ b/clitools/pkg/controller/osupgrade/handler.go
@@ -41,26 +41,55 @@ func (r *UpgradeRunner) Run(fn func() error) error {
 	return fn()
 }
 
-func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
-	namespace string, nodeName string,
-	osu *monov1alpha1.OSUpgrade,
+func HandleOSUpgradeProgress(
+	ctx context.Context,
+	clients *kube.Clients,
+	namespace string,
+	nodeName string,
+	osup *monov1alpha1.OSUpgradeProgress,
 ) error {
 	return r.Run(func() error {
-		return handleOSUpgradeLocked(ctx, clients, namespace, nodeName, osu)
+		return handleOSUpgradeProgressLocked(ctx, clients, namespace, nodeName, osup)
 	})
 }
 
-func handleOSUpgradeLocked(ctx context.Context, clients *kube.Clients,
-	namespace string, nodeName string,
-	osu *monov1alpha1.OSUpgrade,
+func handleOSUpgradeProgressLocked(
+	ctx context.Context,
+	clients *kube.Clients,
+	namespace string,
+	nodeName string,
+	osup *monov1alpha1.OSUpgradeProgress,
 ) error {
-	osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu)
+	if osup == nil {
+		return fmt.Errorf("osupgradeprogress is nil")
+	}
+
+	if osup.Spec.NodeName != nodeName {
+		return nil
+	}
+
+	if osup.Status.Phase != "" &&
+		osup.Status.Phase != monov1alpha1.OSUpgradeProgressPhasePending &&
+		osup.Status.Phase != monov1alpha1.OSUpgradeProgressPhaseDownloading {
+		// tune this logic however you want
+		return nil
+	}
+
+	parentName := osup.Spec.SourceRef.Name
+	if parentName == "" {
+		return failProgress(ctx, clients, osup, "resolve parent osupgrade", fmt.Errorf("missing spec.osUpgradeName"))
+	}
+
+	osu, err := clients.MonoKS.Monok8sV1alpha1().
+		OSUpgrades(namespace).
+		Get(ctx, parentName, metav1.GetOptions{})
 	if err != nil {
-		return err
+		return failProgress(ctx, clients, osup, "resolve parent osupgrade", err)
 	}
 
-	klog.InfoS("handling osupgrade",
-		"name", osu.Name,
+	klog.InfoS("handling osupgradeprogress",
+		"name", osup.Name,
+		"osupgrade", osu.Name,
 		"node", nodeName,
 		"desiredVersion", osu.Spec.DesiredVersion,
 	)
diff --git a/clitools/pkg/controller/osupgrade/progress.go b/clitools/pkg/controller/osupgrade/progress.go
index 40771fe..c537b6c 100644
--- a/clitools/pkg/controller/osupgrade/progress.go
+++ b/clitools/pkg/controller/osupgrade/progress.go
@@ -28,6 +28,33 @@ var (
 	}
 )
 
+func EnsureOSUpgradeProgressForNode(
+	ctx context.Context,
+	clients *kube.Clients,
+	namespace string,
+	nodeName string,
+	osu *monov1alpha1.OSUpgrade,
+) error {
+	if osu == nil {
+		return fmt.Errorf("osupgrade is nil")
+	}
+
+	// Keep using your existing helper if it already encapsulates
+	// selector matching / parent linkage / create-or-update semantics.
+	osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu)
+	if err != nil {
+		return err
+	}
+
+	klog.InfoS("ensured OSUpgradeProgress from admission",
+		"osupgrade", osu.Name,
+		"osupgradeProgress", osup.Name,
+		"node", nodeName,
+	)
+
+	return nil
+}
+
 func ensureProgressHeartbeat(ctx context.Context, clients *kube.Clients,
 	namespace string, nodeName string,
 	osu *monov1alpha1.OSUpgrade,
diff --git a/clitools/pkg/generated/clientset/versioned/clientset.go b/clitools/pkg/generated/clientset/versioned/clientset.go
new file mode 100644
index 0000000..06bd6bd
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/clientset.go
@@ -0,0 +1,106 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package versioned
+
+import (
+	fmt "fmt"
+	http "net/http"
+
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1"
+	discovery "k8s.io/client-go/discovery"
+	rest "k8s.io/client-go/rest"
+	flowcontrol "k8s.io/client-go/util/flowcontrol"
+)
+
+type Interface interface {
+	Discovery() discovery.DiscoveryInterface
+	Monok8sV1alpha1() monok8sv1alpha1.Monok8sV1alpha1Interface
+}
+
+// Clientset contains the clients for groups.
+type Clientset struct {
+	*discovery.DiscoveryClient
+	monok8sV1alpha1 *monok8sv1alpha1.Monok8sV1alpha1Client
+}
+
+// Monok8sV1alpha1 retrieves the Monok8sV1alpha1Client
+func (c *Clientset) Monok8sV1alpha1() monok8sv1alpha1.Monok8sV1alpha1Interface {
+	return c.monok8sV1alpha1
+}
+
+// Discovery retrieves the DiscoveryClient
+func (c *Clientset) Discovery() discovery.DiscoveryInterface {
+	if c == nil {
+		return nil
+	}
+	return c.DiscoveryClient
+}
+
+// NewForConfig creates a new Clientset for the given config.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfig will generate a rate-limiter in configShallowCopy.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*Clientset, error) {
+	configShallowCopy := *c
+
+	if configShallowCopy.UserAgent == "" {
+		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	// share the transport between all clients
+	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewForConfigAndClient(&configShallowCopy, httpClient)
+}
+
+// NewForConfigAndClient creates a new Clientset for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfigAndClient will generate a rate-limiter in configShallowCopy.
+func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
+	configShallowCopy := *c
+	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
+		if configShallowCopy.Burst <= 0 {
+			return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
+		}
+		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
+	}
+
+	var cs Clientset
+	var err error
+	cs.monok8sV1alpha1, err = monok8sv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
+
+	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
+	return &cs, nil
+}
+
+// NewForConfigOrDie creates a new Clientset for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *Clientset {
+	cs, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return cs
+}
+
+// New creates a new Clientset for the given RESTClient.
+func New(c rest.Interface) *Clientset {
+	var cs Clientset
+	cs.monok8sV1alpha1 = monok8sv1alpha1.New(c)
+
+	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
+	return &cs
+}
diff --git a/clitools/pkg/generated/clientset/versioned/fake/clientset_generated.go b/clitools/pkg/generated/clientset/versioned/fake/clientset_generated.go
new file mode 100644
index 0000000..3d05999
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/fake/clientset_generated.go
@@ -0,0 +1,91 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	clientset "example.com/monok8s/pkg/generated/clientset/versioned"
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1"
+	fakemonok8sv1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/discovery"
+	fakediscovery "k8s.io/client-go/discovery/fake"
+	"k8s.io/client-go/testing"
+)
+
+// NewSimpleClientset returns a clientset that will respond with the provided objects.
+// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
+// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement
+// for a real clientset and is mostly useful in simple unit tests.
+//
+// Deprecated: NewClientset replaces this with support for field management, which significantly improves
+// server side apply testing. NewClientset is only available when apply configurations are generated (e.g.
+// via --with-applyconfig).
+func NewSimpleClientset(objects ...runtime.Object) *Clientset {
+	o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
+	for _, obj := range objects {
+		if err := o.Add(obj); err != nil {
+			panic(err)
+		}
+	}
+
+	cs := &Clientset{tracker: o}
+	cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
+	cs.AddReactor("*", "*", testing.ObjectReaction(o))
+	cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
+		var opts metav1.ListOptions
+		if watchAction, ok := action.(testing.WatchActionImpl); ok {
+			opts = watchAction.ListOptions
+		}
+		gvr := action.GetResource()
+		ns := action.GetNamespace()
+		watch, err := o.Watch(gvr, ns, opts)
+		if err != nil {
+			return false, nil, err
+		}
+		return true, watch, nil
+	})
+
+	return cs
+}
+
+// Clientset implements clientset.Interface. Meant to be embedded into a
+// struct to get a default implementation. This makes faking out just the method
+// you want to test easier.
+type Clientset struct {
+	testing.Fake
+	discovery *fakediscovery.FakeDiscovery
+	tracker   testing.ObjectTracker
+}
+
+func (c *Clientset) Discovery() discovery.DiscoveryInterface {
+	return c.discovery
+}
+
+func (c *Clientset) Tracker() testing.ObjectTracker {
+	return c.tracker
+}
+
+// IsWatchListSemanticsSupported informs the reflector that this client
+// doesn't support WatchList semantics.
+//
+// This is a synthetic method whose sole purpose is to satisfy the optional
+// interface check performed by the reflector.
+// Returning true signals that WatchList can NOT be used.
+// No additional logic is implemented here.
+func (c *Clientset) IsWatchListSemanticsUnSupported() bool {
+	return true
+}
+
+var (
+	_ clientset.Interface = &Clientset{}
+	_ testing.FakeClient  = &Clientset{}
+)
+
+// Monok8sV1alpha1 retrieves the Monok8sV1alpha1Client
+func (c *Clientset) Monok8sV1alpha1() monok8sv1alpha1.Monok8sV1alpha1Interface {
+	return &fakemonok8sv1alpha1.FakeMonok8sV1alpha1{Fake: &c.Fake}
+}
diff --git a/clitools/pkg/generated/clientset/versioned/fake/doc.go b/clitools/pkg/generated/clientset/versioned/fake/doc.go
new file mode 100644
index 0000000..ff3a27c
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/fake/doc.go
@@ -0,0 +1,6 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated fake clientset.
+package fake
diff --git a/clitools/pkg/generated/clientset/versioned/fake/register.go b/clitools/pkg/generated/clientset/versioned/fake/register.go
new file mode 100644
index 0000000..75b5b4f
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/fake/register.go
@@ -0,0 +1,42 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+)
+
+var scheme = runtime.NewScheme()
+var codecs = serializer.NewCodecFactory(scheme)
+
+var localSchemeBuilder = runtime.SchemeBuilder{
+	monok8sv1alpha1.AddToScheme,
+}
+
+// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+// of clientsets, like in:
+//
+//	import (
+//	  "k8s.io/client-go/kubernetes"
+//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//	)
+//
+//	kclientset, _ := kubernetes.NewForConfig(c)
+//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//
+// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
+// correctly.
+var AddToScheme = localSchemeBuilder.AddToScheme
+
+func init() {
+	v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
+	utilruntime.Must(AddToScheme(scheme))
+}
diff --git a/clitools/pkg/generated/clientset/versioned/scheme/doc.go b/clitools/pkg/generated/clientset/versioned/scheme/doc.go
new file mode 100644
index 0000000..7834d39
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/scheme/doc.go
@@ -0,0 +1,6 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package contains the scheme of the automatically generated clientset.
+package scheme
diff --git a/clitools/pkg/generated/clientset/versioned/scheme/register.go b/clitools/pkg/generated/clientset/versioned/scheme/register.go
new file mode 100644
index 0000000..c342a9c
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/scheme/register.go
@@ -0,0 +1,42 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package scheme
+
+import (
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+)
+
+var Scheme = runtime.NewScheme()
+var Codecs = serializer.NewCodecFactory(Scheme)
+var ParameterCodec = runtime.NewParameterCodec(Scheme)
+var localSchemeBuilder = runtime.SchemeBuilder{
+	monok8sv1alpha1.AddToScheme,
+}
+
+// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+// of clientsets, like in:
+//
+//	import (
+//	  "k8s.io/client-go/kubernetes"
+//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//	)
+//
+//	kclientset, _ := kubernetes.NewForConfig(c)
+//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//
+// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
+// correctly.
+var AddToScheme = localSchemeBuilder.AddToScheme
+
+func init() {
+	v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
+	utilruntime.Must(AddToScheme(Scheme))
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/doc.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/doc.go
new file mode 100644
index 0000000..d089847
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/doc.go
@@ -0,0 +1,6 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1alpha1
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/doc.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/doc.go
new file mode 100644
index 0000000..0edf26f
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/doc.go
@@ -0,0 +1,6 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_monok8s_client.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_monok8s_client.go
new file mode 100644
index 0000000..24ce525
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_monok8s_client.go
@@ -0,0 +1,30 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeMonok8sV1alpha1 struct {
+	*testing.Fake
+}
+
+func (c *FakeMonok8sV1alpha1) OSUpgrades(namespace string) v1alpha1.OSUpgradeInterface {
+	return newFakeOSUpgrades(c, namespace)
+}
+
+func (c *FakeMonok8sV1alpha1) OSUpgradeProgresses(namespace string) v1alpha1.OSUpgradeProgressInterface {
+	return newFakeOSUpgradeProgresses(c, namespace)
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeMonok8sV1alpha1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgrade.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgrade.go
new file mode 100644
index 0000000..7ee1874
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgrade.go
@@ -0,0 +1,36 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1"
+	gentype "k8s.io/client-go/gentype"
+)
+
+// fakeOSUpgrades implements OSUpgradeInterface
+type fakeOSUpgrades struct {
+	*gentype.FakeClientWithList[*v1alpha1.OSUpgrade, *v1alpha1.OSUpgradeList]
+	Fake *FakeMonok8sV1alpha1
+}
+
+func newFakeOSUpgrades(fake *FakeMonok8sV1alpha1, namespace string) monok8sv1alpha1.OSUpgradeInterface {
+	return &fakeOSUpgrades{
+		gentype.NewFakeClientWithList[*v1alpha1.OSUpgrade, *v1alpha1.OSUpgradeList](
+			fake.Fake,
+			namespace,
+			v1alpha1.SchemeGroupVersion.WithResource("osupgrades"),
+			v1alpha1.SchemeGroupVersion.WithKind("OSUpgrade"),
+			func() *v1alpha1.OSUpgrade { return &v1alpha1.OSUpgrade{} },
+			func() *v1alpha1.OSUpgradeList { return &v1alpha1.OSUpgradeList{} },
+			func(dst, src *v1alpha1.OSUpgradeList) { dst.ListMeta = src.ListMeta },
+			func(list *v1alpha1.OSUpgradeList) []*v1alpha1.OSUpgrade { return gentype.ToPointerSlice(list.Items) },
+			func(list *v1alpha1.OSUpgradeList, items []*v1alpha1.OSUpgrade) {
+				list.Items = gentype.FromPointerSlice(items)
+			},
+		),
+		fake,
+	}
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgradeprogress.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgradeprogress.go
new file mode 100644
index 0000000..d0fa7f1
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/fake/fake_osupgradeprogress.go
@@ -0,0 +1,38 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1"
+	gentype "k8s.io/client-go/gentype"
+)
+
+// fakeOSUpgradeProgresses implements OSUpgradeProgressInterface
+type fakeOSUpgradeProgresses struct {
+	*gentype.FakeClientWithList[*v1alpha1.OSUpgradeProgress, *v1alpha1.OSUpgradeProgressList]
+	Fake *FakeMonok8sV1alpha1
+}
+
+func newFakeOSUpgradeProgresses(fake *FakeMonok8sV1alpha1, namespace string) monok8sv1alpha1.OSUpgradeProgressInterface {
+	return &fakeOSUpgradeProgresses{
+		gentype.NewFakeClientWithList[*v1alpha1.OSUpgradeProgress, *v1alpha1.OSUpgradeProgressList](
+			fake.Fake,
+			namespace,
+			v1alpha1.SchemeGroupVersion.WithResource("osupgradeprogresses"),
+			v1alpha1.SchemeGroupVersion.WithKind("OSUpgradeProgress"),
+			func() *v1alpha1.OSUpgradeProgress { return &v1alpha1.OSUpgradeProgress{} },
+			func() *v1alpha1.OSUpgradeProgressList { return &v1alpha1.OSUpgradeProgressList{} },
+			func(dst, src *v1alpha1.OSUpgradeProgressList) { dst.ListMeta = src.ListMeta },
+			func(list *v1alpha1.OSUpgradeProgressList) []*v1alpha1.OSUpgradeProgress {
+				return gentype.ToPointerSlice(list.Items)
+			},
+			func(list *v1alpha1.OSUpgradeProgressList, items []*v1alpha1.OSUpgradeProgress) {
+				list.Items = gentype.FromPointerSlice(items)
+			},
+		),
+		fake,
+	}
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/generated_expansion.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/generated_expansion.go
new file mode 100644
index 0000000..5b936f5
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/generated_expansion.go
@@ -0,0 +1,9 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+type OSUpgradeExpansion interface{}
+
+type OSUpgradeProgressExpansion interface{}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/monok8s_client.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/monok8s_client.go
new file mode 100644
index 0000000..7c3a7a7
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/monok8s_client.go
@@ -0,0 +1,92 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	http "net/http"
+
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	scheme "example.com/monok8s/pkg/generated/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type Monok8sV1alpha1Interface interface {
+	RESTClient() rest.Interface
+	OSUpgradesGetter
+	OSUpgradeProgressesGetter
+}
+
+// Monok8sV1alpha1Client is used to interact with features provided by the monok8s group.
+type Monok8sV1alpha1Client struct {
+	restClient rest.Interface
+}
+
+func (c *Monok8sV1alpha1Client) OSUpgrades(namespace string) OSUpgradeInterface {
+	return newOSUpgrades(c, namespace)
+}
+
+func (c *Monok8sV1alpha1Client) OSUpgradeProgresses(namespace string) OSUpgradeProgressInterface {
+	return newOSUpgradeProgresses(c, namespace)
+}
+
+// NewForConfig creates a new Monok8sV1alpha1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*Monok8sV1alpha1Client, error) {
+	config := *c
+	setConfigDefaults(&config)
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new Monok8sV1alpha1Client 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) (*Monok8sV1alpha1Client, error) {
+	config := *c
+	setConfigDefaults(&config)
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &Monok8sV1alpha1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new Monok8sV1alpha1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *Monok8sV1alpha1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new Monok8sV1alpha1Client for the given RESTClient.
+func New(c rest.Interface) *Monok8sV1alpha1Client {
+	return &Monok8sV1alpha1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) {
+	gv := monok8sv1alpha1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *Monok8sV1alpha1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgrade.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgrade.go
new file mode 100644
index 0000000..a0f3cb6
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgrade.go
@@ -0,0 +1,56 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	scheme "example.com/monok8s/pkg/generated/clientset/versioned/scheme"
+	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"
+)
+
+// OSUpgradesGetter has a method to return a OSUpgradeInterface.
+// A group's client should implement this interface.
+type OSUpgradesGetter interface {
+	OSUpgrades(namespace string) OSUpgradeInterface
+}
+
+// OSUpgradeInterface has methods to work with OSUpgrade resources.
+type OSUpgradeInterface interface {
+	Create(ctx context.Context, oSUpgrade *monok8sv1alpha1.OSUpgrade, opts v1.CreateOptions) (*monok8sv1alpha1.OSUpgrade, error)
+	Update(ctx context.Context, oSUpgrade *monok8sv1alpha1.OSUpgrade, opts v1.UpdateOptions) (*monok8sv1alpha1.OSUpgrade, error)
+	// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+	UpdateStatus(ctx context.Context, oSUpgrade *monok8sv1alpha1.OSUpgrade, opts v1.UpdateOptions) (*monok8sv1alpha1.OSUpgrade, 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) (*monok8sv1alpha1.OSUpgrade, error)
+	List(ctx context.Context, opts v1.ListOptions) (*monok8sv1alpha1.OSUpgradeList, 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 *monok8sv1alpha1.OSUpgrade, err error)
+	OSUpgradeExpansion
+}
+
+// oSUpgrades implements OSUpgradeInterface
+type oSUpgrades struct {
+	*gentype.ClientWithList[*monok8sv1alpha1.OSUpgrade, *monok8sv1alpha1.OSUpgradeList]
+}
+
+// newOSUpgrades returns a OSUpgrades
+func newOSUpgrades(c *Monok8sV1alpha1Client, namespace string) *oSUpgrades {
+	return &oSUpgrades{
+		gentype.NewClientWithList[*monok8sv1alpha1.OSUpgrade, *monok8sv1alpha1.OSUpgradeList](
+			"osupgrades",
+			c.RESTClient(),
+			scheme.ParameterCodec,
+			namespace,
+			func() *monok8sv1alpha1.OSUpgrade { return &monok8sv1alpha1.OSUpgrade{} },
+			func() *monok8sv1alpha1.OSUpgradeList { return &monok8sv1alpha1.OSUpgradeList{} },
+		),
+	}
+}
diff --git a/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgradeprogress.go b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgradeprogress.go
new file mode 100644
index 0000000..8526e6f
--- /dev/null
+++ b/clitools/pkg/generated/clientset/versioned/typed/monok8s/v1alpha1/osupgradeprogress.go
@@ -0,0 +1,56 @@
+/* MIT License */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	scheme "example.com/monok8s/pkg/generated/clientset/versioned/scheme"
+	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"
+)
+
+// OSUpgradeProgressesGetter has a method to return a OSUpgradeProgressInterface.
+// A group's client should implement this interface.
+type OSUpgradeProgressesGetter interface {
+	OSUpgradeProgresses(namespace string) OSUpgradeProgressInterface
+}
+
+// OSUpgradeProgressInterface has methods to work with OSUpgradeProgress resources.
+type OSUpgradeProgressInterface interface {
+	Create(ctx context.Context, oSUpgradeProgress *monok8sv1alpha1.OSUpgradeProgress, opts v1.CreateOptions) (*monok8sv1alpha1.OSUpgradeProgress, error)
+	Update(ctx context.Context, oSUpgradeProgress *monok8sv1alpha1.OSUpgradeProgress, opts v1.UpdateOptions) (*monok8sv1alpha1.OSUpgradeProgress, error)
+	// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+	UpdateStatus(ctx context.Context, oSUpgradeProgress *monok8sv1alpha1.OSUpgradeProgress, opts v1.UpdateOptions) (*monok8sv1alpha1.OSUpgradeProgress, 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) (*monok8sv1alpha1.OSUpgradeProgress, error)
+	List(ctx context.Context, opts v1.ListOptions) (*monok8sv1alpha1.OSUpgradeProgressList, 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 *monok8sv1alpha1.OSUpgradeProgress, err error)
+	OSUpgradeProgressExpansion
+}
+
+// oSUpgradeProgresses implements OSUpgradeProgressInterface
+type oSUpgradeProgresses struct {
+	*gentype.ClientWithList[*monok8sv1alpha1.OSUpgradeProgress, *monok8sv1alpha1.OSUpgradeProgressList]
+}
+
+// newOSUpgradeProgresses returns a OSUpgradeProgresses
+func newOSUpgradeProgresses(c *Monok8sV1alpha1Client, namespace string) *oSUpgradeProgresses {
+	return &oSUpgradeProgresses{
+		gentype.NewClientWithList[*monok8sv1alpha1.OSUpgradeProgress, *monok8sv1alpha1.OSUpgradeProgressList](
+			"osupgradeprogresses",
+			c.RESTClient(),
+			scheme.ParameterCodec,
+			namespace,
+			func() *monok8sv1alpha1.OSUpgradeProgress { return &monok8sv1alpha1.OSUpgradeProgress{} },
+			func() *monok8sv1alpha1.OSUpgradeProgressList { return &monok8sv1alpha1.OSUpgradeProgressList{} },
+		),
+	}
+}
diff --git a/clitools/pkg/generated/informers/externalversions/factory.go b/clitools/pkg/generated/informers/externalversions/factory.go
new file mode 100644
index 0000000..55210f7
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/factory.go
@@ -0,0 +1,249 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package externalversions
+
+import (
+	reflect "reflect"
+	sync "sync"
+	time "time"
+
+	versioned "example.com/monok8s/pkg/generated/clientset/versioned"
+	internalinterfaces "example.com/monok8s/pkg/generated/informers/externalversions/internalinterfaces"
+	monok8s "example.com/monok8s/pkg/generated/informers/externalversions/monok8s"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// SharedInformerOption defines the functional option type for SharedInformerFactory.
+type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
+
+type sharedInformerFactory struct {
+	client           versioned.Interface
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	lock             sync.Mutex
+	defaultResync    time.Duration
+	customResync     map[reflect.Type]time.Duration
+	transform        cache.TransformFunc
+
+	informers map[reflect.Type]cache.SharedIndexInformer
+	// startedInformers is used for tracking which informers have been started.
+	// This allows Start() to be called multiple times safely.
+	startedInformers map[reflect.Type]bool
+	// wg tracks how many goroutines were started.
+	wg sync.WaitGroup
+	// shuttingDown is true when Shutdown has been called. It may still be running
+	// because it needs to wait for goroutines.
+	shuttingDown bool
+}
+
+// WithCustomResyncConfig sets a custom resync period for the specified informer types.
+func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		for k, v := range resyncConfig {
+			factory.customResync[reflect.TypeOf(k)] = v
+		}
+		return factory
+	}
+}
+
+// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
+func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.tweakListOptions = tweakListOptions
+		return factory
+	}
+}
+
+// WithNamespace limits the SharedInformerFactory to the specified namespace.
+func WithNamespace(namespace string) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.namespace = namespace
+		return factory
+	}
+}
+
+// WithTransform sets a transform on all informers.
+func WithTransform(transform cache.TransformFunc) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.transform = transform
+		return factory
+	}
+}
+
+// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
+func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
+	return NewSharedInformerFactoryWithOptions(client, defaultResync)
+}
+
+// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
+// Listers obtained via this SharedInformerFactory will be subject to the same filters
+// as specified here.
+//
+// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
+func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
+	return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
+}
+
+// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
+func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
+	factory := &sharedInformerFactory{
+		client:           client,
+		namespace:        v1.NamespaceAll,
+		defaultResync:    defaultResync,
+		informers:        make(map[reflect.Type]cache.SharedIndexInformer),
+		startedInformers: make(map[reflect.Type]bool),
+		customResync:     make(map[reflect.Type]time.Duration),
+	}
+
+	// Apply all options
+	for _, opt := range options {
+		factory = opt(factory)
+	}
+
+	return factory
+}
+
+func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	if f.shuttingDown {
+		return
+	}
+
+	for informerType, informer := range f.informers {
+		if !f.startedInformers[informerType] {
+			f.wg.Add(1)
+			// We need a new variable in each loop iteration,
+			// otherwise the goroutine would use the loop variable
+			// and that keeps changing.
+			informer := informer
+			go func() {
+				defer f.wg.Done()
+				informer.Run(stopCh)
+			}()
+			f.startedInformers[informerType] = true
+		}
+	}
+}
+
+func (f *sharedInformerFactory) Shutdown() {
+	f.lock.Lock()
+	f.shuttingDown = true
+	f.lock.Unlock()
+
+	// Will return immediately if there is nothing to wait for.
+	f.wg.Wait()
+}
+
+func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
+	informers := func() map[reflect.Type]cache.SharedIndexInformer {
+		f.lock.Lock()
+		defer f.lock.Unlock()
+
+		informers := map[reflect.Type]cache.SharedIndexInformer{}
+		for informerType, informer := range f.informers {
+			if f.startedInformers[informerType] {
+				informers[informerType] = informer
+			}
+		}
+		return informers
+	}()
+
+	res := map[reflect.Type]bool{}
+	for informType, informer := range informers {
+		res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
+	}
+	return res
+}
+
+// InformerFor returns the SharedIndexInformer for obj using an internal
+// client.
+func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	informerType := reflect.TypeOf(obj)
+	informer, exists := f.informers[informerType]
+	if exists {
+		return informer
+	}
+
+	resyncPeriod, exists := f.customResync[informerType]
+	if !exists {
+		resyncPeriod = f.defaultResync
+	}
+
+	informer = newFunc(f.client, resyncPeriod)
+	informer.SetTransform(f.transform)
+	f.informers[informerType] = informer
+
+	return informer
+}
+
+// SharedInformerFactory provides shared informers for resources in all known
+// API group versions.
+//
+// It is typically used like this:
+//
+//	ctx, cancel := context.WithCancel(context.Background())
+//	defer cancel()
+//	factory := NewSharedInformerFactory(client, resyncPeriod)
+//	defer factory.WaitForStop()    // Returns immediately if nothing was started.
+//	genericInformer := factory.ForResource(resource)
+//	typedInformer := factory.SomeAPIGroup().V1().SomeType()
+//	factory.Start(ctx.Done())          // Start processing these informers.
+//	synced := factory.WaitForCacheSync(ctx.Done())
+//	for v, ok := range synced {
+//	    if !ok {
+//	        fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v)
+//	        return
+//	    }
+//	}
+//
+//	// Creating informers can also be created after Start, but then
+//	// Start must be called again:
+//	anotherGenericInformer := factory.ForResource(resource)
+//	factory.Start(ctx.Done())
+type SharedInformerFactory interface {
+	internalinterfaces.SharedInformerFactory
+
+	// Start initializes all requested informers. They are handled in goroutines
+	// which run until the stop channel gets closed.
+	// Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync.
+	Start(stopCh <-chan struct{})
+
+	// Shutdown marks a factory as shutting down. At that point no new
+	// informers can be started anymore and Start will return without
+	// doing anything.
+	//
+	// In addition, Shutdown blocks until all goroutines have terminated. For that
+	// to happen, the close channel(s) that they were started with must be closed,
+	// either before Shutdown gets called or while it is waiting.
+	//
+	// Shutdown may be called multiple times, even concurrently. All such calls will
+	// block until all goroutines have terminated.
+	Shutdown()
+
+	// WaitForCacheSync blocks until all started informers' caches were synced
+	// or the stop channel gets closed.
+	WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
+
+	// ForResource gives generic access to a shared informer of the matching type.
+	ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
+
+	// InformerFor returns the SharedIndexInformer for obj using an internal
+	// client.
+	InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer
+
+	Monok8s() monok8s.Interface
+}
+
+func (f *sharedInformerFactory) Monok8s() monok8s.Interface {
+	return monok8s.New(f, f.namespace, f.tweakListOptions)
+}
diff --git a/clitools/pkg/generated/informers/externalversions/generic.go b/clitools/pkg/generated/informers/externalversions/generic.go
new file mode 100644
index 0000000..d3b53a5
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/generic.go
@@ -0,0 +1,50 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package externalversions
+
+import (
+	fmt "fmt"
+
+	v1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
+// sharedInformers based on type
+type GenericInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() cache.GenericLister
+}
+
+type genericInformer struct {
+	informer cache.SharedIndexInformer
+	resource schema.GroupResource
+}
+
+// Informer returns the SharedIndexInformer.
+func (f *genericInformer) Informer() cache.SharedIndexInformer {
+	return f.informer
+}
+
+// Lister returns the GenericLister.
+func (f *genericInformer) Lister() cache.GenericLister {
+	return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
+}
+
+// ForResource gives generic access to a shared informer of the matching type
+// TODO extend this to unknown resources with a client pool
+func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
+	switch resource {
+	// Group=monok8s, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("osupgrades"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Monok8s().V1alpha1().OSUpgrades().Informer()}, nil
+	case v1alpha1.SchemeGroupVersion.WithResource("osupgradeprogresses"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Monok8s().V1alpha1().OSUpgradeProgresses().Informer()}, nil
+
+	}
+
+	return nil, fmt.Errorf("no informer found for %v", resource)
+}
diff --git a/clitools/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/clitools/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go
new file mode 100644
index 0000000..c963afb
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go
@@ -0,0 +1,26 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package internalinterfaces
+
+import (
+	time "time"
+
+	versioned "example.com/monok8s/pkg/generated/clientset/versioned"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer.
+type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
+
+// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
+type SharedInformerFactory interface {
+	Start(stopCh <-chan struct{})
+	InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
+}
+
+// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
+type TweakListOptionsFunc func(*v1.ListOptions)
diff --git a/clitools/pkg/generated/informers/externalversions/monok8s/interface.go b/clitools/pkg/generated/informers/externalversions/monok8s/interface.go
new file mode 100644
index 0000000..9e39f5d
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/monok8s/interface.go
@@ -0,0 +1,32 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package monok8s
+
+import (
+	internalinterfaces "example.com/monok8s/pkg/generated/informers/externalversions/internalinterfaces"
+	v1alpha1 "example.com/monok8s/pkg/generated/informers/externalversions/monok8s/v1alpha1"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1alpha1 provides access to shared informers for resources in V1alpha1.
+	V1alpha1() v1alpha1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1alpha1 returns a new v1alpha1.Interface.
+func (g *group) V1alpha1() v1alpha1.Interface {
+	return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/interface.go b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/interface.go
new file mode 100644
index 0000000..4cb8d11
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/interface.go
@@ -0,0 +1,38 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	internalinterfaces "example.com/monok8s/pkg/generated/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// OSUpgrades returns a OSUpgradeInformer.
+	OSUpgrades() OSUpgradeInformer
+	// OSUpgradeProgresses returns a OSUpgradeProgressInformer.
+	OSUpgradeProgresses() OSUpgradeProgressInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// OSUpgrades returns a OSUpgradeInformer.
+func (v *version) OSUpgrades() OSUpgradeInformer {
+	return &oSUpgradeInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
+// OSUpgradeProgresses returns a OSUpgradeProgressInformer.
+func (v *version) OSUpgradeProgresses() OSUpgradeProgressInformer {
+	return &oSUpgradeProgressInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgrade.go b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgrade.go
new file mode 100644
index 0000000..036b00a
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgrade.go
@@ -0,0 +1,88 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+	time "time"
+
+	apismonok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	versioned "example.com/monok8s/pkg/generated/clientset/versioned"
+	internalinterfaces "example.com/monok8s/pkg/generated/informers/externalversions/internalinterfaces"
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/listers/monok8s/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// OSUpgradeInformer provides access to a shared informer and lister for
+// OSUpgrades.
+type OSUpgradeInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() monok8sv1alpha1.OSUpgradeLister
+}
+
+type oSUpgradeInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewOSUpgradeInformer constructs a new informer for OSUpgrade type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewOSUpgradeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredOSUpgradeInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredOSUpgradeInformer constructs a new informer for OSUpgrade type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredOSUpgradeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgrades(namespace).List(context.Background(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgrades(namespace).Watch(context.Background(), options)
+			},
+			ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgrades(namespace).List(ctx, options)
+			},
+			WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgrades(namespace).Watch(ctx, options)
+			},
+		}, client),
+		&apismonok8sv1alpha1.OSUpgrade{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *oSUpgradeInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredOSUpgradeInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *oSUpgradeInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&apismonok8sv1alpha1.OSUpgrade{}, f.defaultInformer)
+}
+
+func (f *oSUpgradeInformer) Lister() monok8sv1alpha1.OSUpgradeLister {
+	return monok8sv1alpha1.NewOSUpgradeLister(f.Informer().GetIndexer())
+}
diff --git a/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgradeprogress.go b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgradeprogress.go
new file mode 100644
index 0000000..5995e3e
--- /dev/null
+++ b/clitools/pkg/generated/informers/externalversions/monok8s/v1alpha1/osupgradeprogress.go
@@ -0,0 +1,88 @@
+/* MIT License */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+	time "time"
+
+	apismonok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	versioned "example.com/monok8s/pkg/generated/clientset/versioned"
+	internalinterfaces "example.com/monok8s/pkg/generated/informers/externalversions/internalinterfaces"
+	monok8sv1alpha1 "example.com/monok8s/pkg/generated/listers/monok8s/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// OSUpgradeProgressInformer provides access to a shared informer and lister for
+// OSUpgradeProgresses.
+type OSUpgradeProgressInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() monok8sv1alpha1.OSUpgradeProgressLister
+}
+
+type oSUpgradeProgressInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewOSUpgradeProgressInformer constructs a new informer for OSUpgradeProgress type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewOSUpgradeProgressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredOSUpgradeProgressInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredOSUpgradeProgressInformer constructs a new informer for OSUpgradeProgress type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredOSUpgradeProgressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgradeProgresses(namespace).List(context.Background(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgradeProgresses(namespace).Watch(context.Background(), options)
+			},
+			ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgradeProgresses(namespace).List(ctx, options)
+			},
+			WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.Monok8sV1alpha1().OSUpgradeProgresses(namespace).Watch(ctx, options)
+			},
+		}, client),
+		&apismonok8sv1alpha1.OSUpgradeProgress{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *oSUpgradeProgressInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredOSUpgradeProgressInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *oSUpgradeProgressInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&apismonok8sv1alpha1.OSUpgradeProgress{}, f.defaultInformer)
+}
+
+func (f *oSUpgradeProgressInformer) Lister() monok8sv1alpha1.OSUpgradeProgressLister {
+	return monok8sv1alpha1.NewOSUpgradeProgressLister(f.Informer().GetIndexer())
+}
diff --git a/clitools/pkg/generated/listers/monok8s/v1alpha1/expansion_generated.go b/clitools/pkg/generated/listers/monok8s/v1alpha1/expansion_generated.go
new file mode 100644
index 0000000..8579589
--- /dev/null
+++ b/clitools/pkg/generated/listers/monok8s/v1alpha1/expansion_generated.go
@@ -0,0 +1,21 @@
+/* MIT License */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// OSUpgradeListerExpansion allows custom methods to be added to
+// OSUpgradeLister.
+type OSUpgradeListerExpansion interface{}
+
+// OSUpgradeNamespaceListerExpansion allows custom methods to be added to
+// OSUpgradeNamespaceLister.
+type OSUpgradeNamespaceListerExpansion interface{}
+
+// OSUpgradeProgressListerExpansion allows custom methods to be added to
+// OSUpgradeProgressLister.
+type OSUpgradeProgressListerExpansion interface{}
+
+// OSUpgradeProgressNamespaceListerExpansion allows custom methods to be added to
+// OSUpgradeProgressNamespaceLister.
+type OSUpgradeProgressNamespaceListerExpansion interface{}
diff --git a/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgrade.go b/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgrade.go
new file mode 100644
index 0000000..0271250
--- /dev/null
+++ b/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgrade.go
@@ -0,0 +1,56 @@
+/* MIT License */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	listers "k8s.io/client-go/listers"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// OSUpgradeLister helps list OSUpgrades.
+// All objects returned here must be treated as read-only.
+type OSUpgradeLister interface {
+	// List lists all OSUpgrades in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*monok8sv1alpha1.OSUpgrade, err error)
+	// OSUpgrades returns an object that can list and get OSUpgrades.
+	OSUpgrades(namespace string) OSUpgradeNamespaceLister
+	OSUpgradeListerExpansion
+}
+
+// oSUpgradeLister implements the OSUpgradeLister interface.
+type oSUpgradeLister struct {
+	listers.ResourceIndexer[*monok8sv1alpha1.OSUpgrade]
+}
+
+// NewOSUpgradeLister returns a new OSUpgradeLister.
+func NewOSUpgradeLister(indexer cache.Indexer) OSUpgradeLister {
+	return &oSUpgradeLister{listers.New[*monok8sv1alpha1.OSUpgrade](indexer, monok8sv1alpha1.Resource("osupgrade"))}
+}
+
+// OSUpgrades returns an object that can list and get OSUpgrades.
+func (s *oSUpgradeLister) OSUpgrades(namespace string) OSUpgradeNamespaceLister {
+	return oSUpgradeNamespaceLister{listers.NewNamespaced[*monok8sv1alpha1.OSUpgrade](s.ResourceIndexer, namespace)}
+}
+
+// OSUpgradeNamespaceLister helps list and get OSUpgrades.
+// All objects returned here must be treated as read-only.
+type OSUpgradeNamespaceLister interface {
+	// List lists all OSUpgrades in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*monok8sv1alpha1.OSUpgrade, err error)
+	// Get retrieves the OSUpgrade from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*monok8sv1alpha1.OSUpgrade, error)
+	OSUpgradeNamespaceListerExpansion
+}
+
+// oSUpgradeNamespaceLister implements the OSUpgradeNamespaceLister
+// interface.
+type oSUpgradeNamespaceLister struct {
+	listers.ResourceIndexer[*monok8sv1alpha1.OSUpgrade]
+}
diff --git a/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgradeprogress.go b/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgradeprogress.go
new file mode 100644
index 0000000..5e60e2d
--- /dev/null
+++ b/clitools/pkg/generated/listers/monok8s/v1alpha1/osupgradeprogress.go
@@ -0,0 +1,56 @@
+/* MIT License */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	monok8sv1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	listers "k8s.io/client-go/listers"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// OSUpgradeProgressLister helps list OSUpgradeProgresses.
+// All objects returned here must be treated as read-only.
+type OSUpgradeProgressLister interface {
+	// List lists all OSUpgradeProgresses in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*monok8sv1alpha1.OSUpgradeProgress, err error)
+	// OSUpgradeProgresses returns an object that can list and get OSUpgradeProgresses.
+	OSUpgradeProgresses(namespace string) OSUpgradeProgressNamespaceLister
+	OSUpgradeProgressListerExpansion
+}
+
+// oSUpgradeProgressLister implements the OSUpgradeProgressLister interface.
+type oSUpgradeProgressLister struct {
+	listers.ResourceIndexer[*monok8sv1alpha1.OSUpgradeProgress]
+}
+
+// NewOSUpgradeProgressLister returns a new OSUpgradeProgressLister.
+func NewOSUpgradeProgressLister(indexer cache.Indexer) OSUpgradeProgressLister {
+	return &oSUpgradeProgressLister{listers.New[*monok8sv1alpha1.OSUpgradeProgress](indexer, monok8sv1alpha1.Resource("osupgradeprogress"))}
+}
+
+// OSUpgradeProgresses returns an object that can list and get OSUpgradeProgresses.
+func (s *oSUpgradeProgressLister) OSUpgradeProgresses(namespace string) OSUpgradeProgressNamespaceLister {
+	return oSUpgradeProgressNamespaceLister{listers.NewNamespaced[*monok8sv1alpha1.OSUpgradeProgress](s.ResourceIndexer, namespace)}
+}
+
+// OSUpgradeProgressNamespaceLister helps list and get OSUpgradeProgresses.
+// All objects returned here must be treated as read-only.
+type OSUpgradeProgressNamespaceLister interface {
+	// List lists all OSUpgradeProgresses in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*monok8sv1alpha1.OSUpgradeProgress, err error)
+	// Get retrieves the OSUpgradeProgress from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*monok8sv1alpha1.OSUpgradeProgress, error)
+	OSUpgradeProgressNamespaceListerExpansion
+}
+
+// oSUpgradeProgressNamespaceLister implements the OSUpgradeProgressNamespaceLister
+// interface.
+type oSUpgradeProgressNamespaceLister struct {
+	listers.ResourceIndexer[*monok8sv1alpha1.OSUpgradeProgress]
+}
diff --git a/clitools/pkg/kube/clients.go b/clitools/pkg/kube/clients.go
index 6c3a814..38491ac 100644
--- a/clitools/pkg/kube/clients.go
+++ b/clitools/pkg/kube/clients.go
@@ -3,11 +3,12 @@ package kube
 import (
 	"fmt"
 
+	monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	monoclientset "example.com/monok8s/pkg/generated/clientset/versioned"
 	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	kubernetes "k8s.io/client-go/kubernetes"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
-	monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -21,6 +22,7 @@ type Clients struct {
 	Dynamic          dynamic.Interface
 	APIExtensions    apiextensionsclientset.Interface
 	RESTClientGetter genericclioptions.RESTClientGetter
+	MonoKS           monoclientset.Interface
 }
 
 func NewClients(flags *genericclioptions.ConfigFlags) (*Clients, error) {
@@ -40,7 +42,19 @@ func NewClients(flags *genericclioptions.ConfigFlags) (*Clients, error) {
 	if err != nil {
 		return nil, fmt.Errorf("build apiextensions client: %w", err)
 	}
-	return &Clients{Config: cfg, Kubernetes: kubeClient, Dynamic: dyn, APIExtensions: ext, RESTClientGetter: flags}, nil
+	mono, err := monoclientset.NewForConfig(cfg)
+	if err != nil {
+		return nil, fmt.Errorf("build monok8s client: %w", err)
+	}
+
+	return &Clients{
+		Config:           cfg,
+		Kubernetes:       kubeClient,
+		Dynamic:          dyn,
+		APIExtensions:    ext,
+		RESTClientGetter: flags,
+		MonoKS:           mono,
+	}, nil
 }
 
 func NewClientsFromKubeconfig(kubeconfigPath string) (*Clients, error) {
diff --git a/clitools/pkg/node/admission.go b/clitools/pkg/node/admission.go
new file mode 100644
index 0000000..61b36a4
--- /dev/null
+++ b/clitools/pkg/node/admission.go
@@ -0,0 +1,400 @@
+package node
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"sort"
+	"strings"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/klog/v2"
+
+	monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
+	"example.com/monok8s/pkg/kube"
+	templates "example.com/monok8s/pkg/templates"
+)
+
+func applyAdmissionControllerDeploymentResources(ctx context.Context, n *NodeContext) error {
+	if strings.TrimSpace(n.Config.Spec.ClusterRole) != "control-plane" || !n.Config.Spec.EnableControlAgent {
+		klog.InfoS("skipped admission controller deployment",
+			"clusterRole", n.Config.Spec.ClusterRole,
+			"enableControlAgent", n.Config.Spec.EnableControlAgent,
+		)
+		return nil
+	}
+
+	if err := ApplyCRDs(ctx, n); err != nil {
+		return err
+	}
+
+	namespace := strings.TrimSpace(n.Config.Namespace)
+	if namespace == "" {
+		namespace = templates.DefaultNamespace
+	}
+
+	clients, err := kube.NewClientsFromKubeconfig(adminKubeconfigPath)
+	if err != nil {
+		return fmt.Errorf("build kube clients from %s: %w", adminKubeconfigPath, err)
+	}
+
+	labels := map[string]string{
+		"app.kubernetes.io/name":       controlAgentName,
+		"app.kubernetes.io/component":  "controller",
+		"app.kubernetes.io/part-of":    "monok8s",
+		"app.kubernetes.io/managed-by": "ctl",
+	}
+
+	kubeClient := clients.Kubernetes
+
+	if err := ensureNamespace(ctx, kubeClient, namespace, labels); err != nil {
+		return fmt.Errorf("ensure namespace %q: %w", namespace, err)
+	}
+	if err := applyAdmissionControllerServiceAccount(ctx, kubeClient, namespace, labels); err != nil {
+		return fmt.Errorf("apply serviceaccount: %w", err)
+	}
+	if err := applyAdmissionControllerClusterRole(ctx, kubeClient, labels); err != nil {
+		return fmt.Errorf("apply clusterrole: %w", err)
+	}
+	if err := applyAdmissionControllerClusterRoleBinding(ctx, kubeClient, namespace, labels); err != nil {
+		return fmt.Errorf("apply clusterrolebinding: %w", err)
+	}
+	if err := applyAdmissionControllerDeployment(ctx, kubeClient, namespace, labels); err != nil {
+		return fmt.Errorf("apply deployment: %w", err)
+	}
+
+	return nil
+}
+
+func applyAdmissionControllerServiceAccount(ctx context.Context, kubeClient kubernetes.Interface, namespace string, labels map[string]string) error {
+	automount := true
+
+	want := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      controlAgentName,
+			Namespace: namespace,
+			Labels:    labels,
+		},
+		AutomountServiceAccountToken: &automount,
+	}
+
+	existing, err := kubeClient.CoreV1().ServiceAccounts(namespace).Get(ctx, controlAgentName, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		_, err = kubeClient.CoreV1().ServiceAccounts(namespace).Create(ctx, want, metav1.CreateOptions{})
+		return err
+	}
+	if err != nil {
+		return err
+	}
+
+	changed := false
+	if !reflect.DeepEqual(existing.Labels, want.Labels) {
+		existing.Labels = want.Labels
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.AutomountServiceAccountToken, want.AutomountServiceAccountToken) {
+		existing.AutomountServiceAccountToken = want.AutomountServiceAccountToken
+		changed = true
+	}
+
+	if !changed {
+		return nil
+	}
+
+	_, err = kubeClient.CoreV1().ServiceAccounts(namespace).Update(ctx, existing, metav1.UpdateOptions{})
+	return err
+}
+
+func applyAdmissionControllerClusterRole(ctx context.Context, kubeClient kubernetes.Interface, labels map[string]string) error {
+	wantRules := []rbacv1.PolicyRule{
+		{
+			APIGroups: []string{monov1alpha1.Group},
+			Resources: []string{"osupgrades"},
+			Verbs:     []string{"get", "list", "watch"},
+		},
+		{
+			APIGroups: []string{monov1alpha1.Group},
+			Resources: []string{"osupgrades/status"},
+			Verbs:     []string{"get", "patch", "update"},
+		},
+		{
+			APIGroups: []string{monov1alpha1.Group},
+			Resources: []string{"osupgradeprogresses"},
+			Verbs:     []string{"get", "list", "watch", "create", "patch", "update"},
+		},
+		{
+			APIGroups: []string{""},
+			Resources: []string{"nodes"},
+			Verbs:     []string{"get", "list", "watch"},
+		},
+	}
+
+	want := &rbacv1.ClusterRole{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   controlAgentName,
+			Labels: labels,
+		},
+		Rules: wantRules,
+	}
+
+	existing, err := kubeClient.RbacV1().ClusterRoles().Get(ctx, controlAgentName, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		_, err = kubeClient.RbacV1().ClusterRoles().Create(ctx, want, metav1.CreateOptions{})
+		return err
+	}
+	if err != nil {
+		return err
+	}
+
+	changed := false
+	if !reflect.DeepEqual(existing.Labels, want.Labels) {
+		existing.Labels = want.Labels
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Rules, want.Rules) {
+		existing.Rules = want.Rules
+		changed = true
+	}
+
+	if !changed {
+		return nil
+	}
+
+	_, err = kubeClient.RbacV1().ClusterRoles().Update(ctx, existing, metav1.UpdateOptions{})
+	return err
+}
+
+func applyAdmissionControllerClusterRoleBinding(ctx context.Context, kubeClient kubernetes.Interface, namespace string, labels map[string]string) error {
+	wantSubjects := []rbacv1.Subject{
+		{
+			Kind:      "ServiceAccount",
+			Name:      controlAgentName,
+			Namespace: namespace,
+		},
+	}
+
+	wantRoleRef := rbacv1.RoleRef{
+		APIGroup: rbacv1.GroupName,
+		Kind:     "ClusterRole",
+		Name:     controlAgentName,
+	}
+
+	want := &rbacv1.ClusterRoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   controlAgentName,
+			Labels: labels,
+		},
+		Subjects: wantSubjects,
+		RoleRef:  wantRoleRef,
+	}
+
+	existing, err := kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, controlAgentName, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		_, err = kubeClient.RbacV1().ClusterRoleBindings().Create(ctx, want, metav1.CreateOptions{})
+		return err
+	}
+	if err != nil {
+		return err
+	}
+
+	changed := false
+	if !reflect.DeepEqual(existing.Labels, want.Labels) {
+		existing.Labels = want.Labels
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Subjects, want.Subjects) {
+		existing.Subjects = want.Subjects
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.RoleRef, want.RoleRef) {
+		existing.RoleRef = want.RoleRef
+		changed = true
+	}
+
+	if !changed {
+		return nil
+	}
+
+	_, err = kubeClient.RbacV1().ClusterRoleBindings().Update(ctx, existing, metav1.UpdateOptions{})
+	return err
+}
+
+func applyAdmissionControllerDeployment(ctx context.Context, kubeClient kubernetes.Interface, namespace string, labels map[string]string) error {
+	replicas := int32(1)
+
+	selectorLabels := map[string]string{
+		"app.kubernetes.io/name":      controlAgentName,
+		"app.kubernetes.io/component": "controller",
+	}
+
+	podLabels := mergeStringMaps(labels, selectorLabels)
+
+	runAsNonRoot := true
+	allowPrivilegeEscalation := false
+
+	want := &appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      controlAgentName,
+			Namespace: namespace,
+			Labels:    labels,
+		},
+		Spec: appsv1.DeploymentSpec{
+			Replicas: &replicas,
+			Selector: &metav1.LabelSelector{
+				MatchLabels: selectorLabels,
+			},
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: podLabels,
+				},
+				Spec: corev1.PodSpec{
+					ServiceAccountName: controlAgentName,
+					Containers: []corev1.Container{
+						{
+							Name:            "controller",
+							Image:           controlAgentImage,
+							ImagePullPolicy: corev1.PullIfNotPresent,
+							Args: []string{
+								"controller",
+							},
+							Env: []corev1.EnvVar{
+								{
+									Name: "POD_NAME",
+									ValueFrom: &corev1.EnvVarSource{
+										FieldRef: &corev1.ObjectFieldSelector{
+											APIVersion: "v1",
+											FieldPath:  "metadata.name",
+										},
+									},
+								},
+								{
+									Name: "POD_NAMESPACE",
+									ValueFrom: &corev1.EnvVarSource{
+										FieldRef: &corev1.ObjectFieldSelector{
+											APIVersion: "v1",
+											FieldPath:  "metadata.namespace",
+										},
+									},
+								},
+								{
+									Name: "NODE_NAME",
+									ValueFrom: &corev1.EnvVarSource{
+										FieldRef: &corev1.ObjectFieldSelector{
+											APIVersion: "v1",
+											FieldPath:  "spec.nodeName",
+										},
+									},
+								},
+							},
+							Ports: []corev1.ContainerPort{
+								{
+									Name:          "http",
+									ContainerPort: 8080,
+									Protocol:      corev1.ProtocolTCP,
+								},
+							},
+							LivenessProbe: &corev1.Probe{
+								ProbeHandler: corev1.ProbeHandler{
+									HTTPGet: &corev1.HTTPGetAction{
+										Path: "/healthz",
+										Port: intstr.FromString("http"),
+									},
+								},
+							},
+							ReadinessProbe: &corev1.Probe{
+								ProbeHandler: corev1.ProbeHandler{
+									HTTPGet: &corev1.HTTPGetAction{
+										Path: "/readyz",
+										Port: intstr.FromString("http"),
+									},
+								},
+							},
+							SecurityContext: &corev1.SecurityContext{
+								RunAsNonRoot:             &runAsNonRoot,
+								AllowPrivilegeEscalation: &allowPrivilegeEscalation,
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	existing, err := kubeClient.AppsV1().Deployments(namespace).Get(ctx, controlAgentName, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		_, err = kubeClient.AppsV1().Deployments(namespace).Create(ctx, want, metav1.CreateOptions{})
+		return err
+	}
+	if err != nil {
+		return err
+	}
+
+	changed := false
+	if !reflect.DeepEqual(existing.Labels, want.Labels) {
+		existing.Labels = want.Labels
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Spec.Replicas, want.Spec.Replicas) {
+		existing.Spec.Replicas = want.Spec.Replicas
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Spec.Selector, want.Spec.Selector) {
+		existing.Spec.Selector = want.Spec.Selector
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Spec.Template.Labels, want.Spec.Template.Labels) {
+		existing.Spec.Template.Labels = want.Spec.Template.Labels
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Spec.Template.Spec.ServiceAccountName, want.Spec.Template.Spec.ServiceAccountName) {
+		existing.Spec.Template.Spec.ServiceAccountName = want.Spec.Template.Spec.ServiceAccountName
+		changed = true
+	}
+	if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers, want.Spec.Template.Spec.Containers) {
+		existing.Spec.Template.Spec.Containers = want.Spec.Template.Spec.Containers
+		changed = true
+	}
+
+	if !changed {
+		return nil
+	}
+
+	_, err = kubeClient.AppsV1().Deployments(namespace).Update(ctx, existing, metav1.UpdateOptions{})
+	return err
+}
+
+func mergeStringMaps(maps ...map[string]string) map[string]string {
+	var total int
+	for _, m := range maps {
+		total += len(m)
+	}
+	if total == 0 {
+		return nil
+	}
+
+	out := make(map[string]string, total)
+	for _, m := range maps {
+		for k, v := range m {
+			out[k] = v
+		}
+	}
+	return out
+}
+
+func sortedKeys(m map[string]string) []string {
+	if len(m) == 0 {
+		return nil
+	}
+	keys := make([]string, 0, len(m))
+	for k := range m {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	return keys
+}
diff --git a/clitools/pkg/node/agent.go b/clitools/pkg/node/agent.go
index 924c1ab..13c42d8 100644
--- a/clitools/pkg/node/agent.go
+++ b/clitools/pkg/node/agent.go
@@ -58,6 +58,9 @@ func ApplyControlAgentDaemonSetResources(ctx context.Context, n *NodeContext) er
 
 	kubeClient := clients.Kubernetes
 
+	if err := ensureNamespace(ctx, kubeClient, namespace, labels); err != nil {
+		return fmt.Errorf("ensure namespace %q: %w", namespace, err)
+	}
 	if err := applyControlAgentServiceAccount(ctx, kubeClient, namespace, labels); err != nil {
 		return fmt.Errorf("apply serviceaccount: %w", err)
 	}
@@ -74,6 +77,46 @@ func ApplyControlAgentDaemonSetResources(ctx context.Context, n *NodeContext) er
 	return nil
 }
 
+func ensureNamespace(
+	ctx context.Context,
+	kubeClient kubernetes.Interface,
+	namespace string,
+	labels map[string]string,
+) error {
+	_, err := kubeClient.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
+	if err == nil {
+		return nil
+	}
+	if !apierrors.IsNotFound(err) {
+		return fmt.Errorf("get namespace: %w", err)
+	}
+
+	ns := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:   namespace,
+			Labels: copyStringMap(labels),
+		},
+	}
+
+	_, err = kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
+	if err != nil && !apierrors.IsAlreadyExists(err) {
+		return fmt.Errorf("create namespace: %w", err)
+	}
+
+	return nil
+}
+
+func copyStringMap(in map[string]string) map[string]string {
+	if len(in) == 0 {
+		return nil
+	}
+	out := make(map[string]string, len(in))
+	for k, v := range in {
+		out[k] = v
+	}
+	return out
+}
+
 func applyControlAgentServiceAccount(ctx context.Context, kubeClient kubernetes.Interface, namespace string, labels map[string]string) error {
 	want := &corev1.ServiceAccount{
 		ObjectMeta: metav1.ObjectMeta{
@@ -108,16 +151,6 @@ func applyControlAgentServiceAccount(ctx context.Context, kubeClient kubernetes.
 
 func applyControlAgentClusterRole(ctx context.Context, kubeClient kubernetes.Interface, labels map[string]string) error {
 	wantRules := []rbacv1.PolicyRule{
-		{
-			APIGroups: []string{monov1alpha1.Group},
-			Resources: []string{"osupgrades"},
-			Verbs:     []string{"get", "list", "watch"},
-		},
-		{
-			APIGroups: []string{monov1alpha1.Group},
-			Resources: []string{"osupgrades/status"},
-			Verbs:     []string{"get", "patch", "update"},
-		},
 		{
 			APIGroups: []string{monov1alpha1.Group},
 			Resources: []string{"osupgradeprogresses"},
diff --git a/clitools/pkg/node/kubeadm.go b/clitools/pkg/node/kubeadm.go
index 1de4e0f..5a741a7 100644
--- a/clitools/pkg/node/kubeadm.go
+++ b/clitools/pkg/node/kubeadm.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net"
 	"os"
+	"sort"
 	"strings"
 	"time"
 
@@ -464,9 +465,57 @@ func buildNodeRegistration(spec monov1alpha1.MonoKSConfigSpec) NodeRegistrationO
 		)
 	}
 
+	labels := effectiveNodeLabels(spec)
+	if nodeLabels := buildNodeLabelsArg(labels); nodeLabels != "" {
+		nr.KubeletExtraArgs = append(nr.KubeletExtraArgs,
+			KubeadmArg{Name: "node-labels", Value: nodeLabels},
+		)
+	}
+
 	return nr
 }
 
+func effectiveNodeLabels(spec monov1alpha1.MonoKSConfigSpec) map[string]string {
+	if len(spec.NodeLabels) == 0 && !spec.EnableControlAgent {
+		return nil
+	}
+
+	labels := make(map[string]string, len(spec.NodeLabels)+1)
+	for k, v := range spec.NodeLabels {
+		labels[k] = v
+	}
+
+	if spec.EnableControlAgent {
+		labels[monov1alpha1.ControlAgentKey] = "true"
+	}
+
+	return labels
+}
+
+func buildNodeLabelsArg(labels map[string]string) string {
+	if len(labels) == 0 {
+		return ""
+	}
+
+	keys := make([]string, 0, len(labels))
+	for k := range labels {
+		k = strings.TrimSpace(k)
+		if k == "" {
+			continue
+		}
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	parts := make([]string, 0, len(keys))
+	for _, k := range keys {
+		v := strings.TrimSpace(labels[k])
+		parts = append(parts, k+"="+v)
+	}
+
+	return strings.Join(parts, ",")
+}
+
 func maybeAddBootstrapTaint(nr *NodeRegistrationOptions, role string) {
 	if strings.TrimSpace(role) != "worker" {
 		return
diff --git a/clitools/pkg/node/metadata.go b/clitools/pkg/node/metadata.go
index fd46d3d..e403505 100644
--- a/clitools/pkg/node/metadata.go
+++ b/clitools/pkg/node/metadata.go
@@ -17,8 +17,8 @@ import (
 func ApplyLocalNodeMetadataIfPossible(ctx context.Context, nctx *NodeContext) error {
 	spec := nctx.Config.Spec
 
-	if len(spec.NodeAnnotations) == 0 && len(spec.NodeLabels) == 0 {
-		klog.V(4).Infof("No annotations or labels was defined")
+	if len(spec.NodeLabels) == 0 {
+		klog.V(4).Infof("No labels was defined")
 		return nil // nothing to do
 	}
 
@@ -53,9 +53,6 @@ func ApplyLocalNodeMetadataIfPossible(ctx context.Context, nctx *NodeContext) er
 	if node.Labels == nil {
 		node.Labels = make(map[string]string)
 	}
-	if node.Annotations == nil {
-		node.Annotations = make(map[string]string)
-	}
 
 	// Apply labels
 	for k, v := range spec.NodeLabels {
@@ -67,16 +64,11 @@ func ApplyLocalNodeMetadataIfPossible(ctx context.Context, nctx *NodeContext) er
 		node.Labels[monov1alpah1.ControlAgentKey] = controlAgentNodeSelectorValue
 	}
 
-	// Apply annotations
-	for k, v := range spec.NodeAnnotations {
-		node.Annotations[k] = v
-	}
-
 	_, err = client.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{})
 	if err != nil {
 		return fmt.Errorf("update node metadata: %w", err)
 	}
 
-	klog.Infof("applied labels/annotations to node %q", nodeName)
+	klog.Infof("applied labels to node %q", nodeName)
 	return nil
 }
diff --git a/clitools/pkg/templates/templates.go b/clitools/pkg/templates/templates.go
index 0436941..09d6797 100644
--- a/clitools/pkg/templates/templates.go
+++ b/clitools/pkg/templates/templates.go
@@ -7,7 +7,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-const DefaultNamespace = "kube-system"
+const DefaultNamespace = "mono-system"
 
 func DefaultMonoKSConfig(v TemplateValues) monov1alpha1.MonoKSConfig {
 	return monov1alpha1.MonoKSConfig{
@@ -51,7 +51,6 @@ func DefaultMonoKSConfig(v TemplateValues) monov1alpha1.MonoKSConfig {
 
 			SubjectAltNames: copyStringSlice(v.SubjectAltNames),
 			NodeLabels:      copyStringMap(v.NodeLabels),
-			NodeAnnotations: copyStringMap(v.NodeAnnotations),
 
 			Network: monov1alpha1.NetworkSpec{
 				Hostname:         firstNonEmpty(v.Hostname, v.NodeName),
diff --git a/clitools/pkg/templates/values.go b/clitools/pkg/templates/values.go
index 1f1fc04..2bda201 100644
--- a/clitools/pkg/templates/values.go
+++ b/clitools/pkg/templates/values.go
@@ -39,7 +39,6 @@ type TemplateValues struct {
 
 	SubjectAltNames []string
 	NodeLabels      map[string]string
-	NodeAnnotations map[string]string
 }
 
 func defaultTemplateValues() TemplateValues {
@@ -78,9 +77,6 @@ func defaultTemplateValues() TemplateValues {
 		NodeLabels: map[string]string{
 			monov1alpha1.Label: "value",
 		},
-		NodeAnnotations: map[string]string{
-			monov1alpha1.Annotation: "value",
-		},
 	}
 }
 
@@ -129,9 +125,6 @@ func LoadTemplateValuesFromEnv() TemplateValues {
 	if m := parseKeyValueMap(os.Getenv("MKS_NODE_LABELS")); len(m) > 0 {
 		v.NodeLabels = m
 	}
-	if m := parseKeyValueMap(os.Getenv("MKS_NODE_ANNOTATIONS")); len(m) > 0 {
-		v.NodeAnnotations = m
-	}
 
 	return v
 }
diff --git a/configs/cluster.env.default b/configs/cluster.env.default
index fa65f24..7e8517c 100644
--- a/configs/cluster.env.default
+++ b/configs/cluster.env.default
@@ -56,8 +56,7 @@ MKS_CNI_PLUGIN=default
 
 # Node registration metadata
 # Comma-separated key=value pairs
-MKS_NODE_LABELS=topology.kubernetes.io/zone=lab,node.kubernetes.io/instance-type=mono-gateway
-MKS_NODE_ANNOTATIONS=mono.si/board=ls1046a,monok8s.io/image-version=dev
+MKS_NODE_LABELS=topology.kubernetes.io/zone=lab,node.kubernetes.io/instance-type=mono-gateway,mono.si/board=ls1046a
 
 # Optional
 # Extra API server SANs, comma-separated
diff --git a/devtools/test-upgrade.sh b/devtools/test-upgrade.sh
new file mode 100755
index 0000000..a1dd04e
--- /dev/null
+++ b/devtools/test-upgrade.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
+OUT_DIR="$( realpath "$SCRIPT_DIR"/../out/ )"
+
+set -e
+
+BASE_URL="http://localhost:8000"
+TARGET_VERSION="v1.34.6"
+STABLE_VERSION="v1.34.6"
+NAME="my-upgrade-2"
+
+echo "apiVersion: monok8s.io/v1alpha1"
+echo "kind: OSUpgrade"
+echo "metadata:"
+echo "  name: \"$NAME\""
+echo "spec:"
+echo "  desiredVersion: \"$TARGET_VERSION\""
+echo "  imageURL: \"$BASE_URL/monok8s-$TARGET_VERSION-dev.ext4.zst\""
+echo "  nodeSelector: {}"
+echo "  catalog:"
+echo "    inline: |"
+echo "      stable: $STABLE_VERSION"
+echo "      images:"
+
+for c in "$OUT_DIR"/catalog-*.txt; do
+    version=$(grep 'version:' "$c" | awk '{print $3}')
+    url=$(grep 'url:' "$c" | sed 's/.*"\(.*\)"/\1/')
+    checksum=$(grep 'checksum:' "$c" | awk '{print $2}')
+    size=$(grep 'size:' "$c" | awk '{print $2}')
+
+    filename=$(basename "$url")
+
+    echo "        - version: $version"
+    echo "          url: $BASE_URL/$filename"
+    echo "          checksum: sha256:$checksum"
+    echo "          size: $size"
+done
+
+echo "      blocked:"
+echo "        - v1.34.0"