From 22e398449c48cedb1a6ea7a1080fa1bd4c4c453c Mon Sep 17 00:00:00 2001 From: Serge Logvinov Date: Sun, 21 Apr 2024 08:51:56 +0300 Subject: [PATCH] feat: node transformer Node transformer allows you to add extra labels/annotations and modify platform metadata. Signed-off-by: Serge Logvinov --- .github/workflows/build-test.yaml | 6 +- .github/workflows/release-charts.yaml | 2 - .golangci.yml | 30 ++- .../talos-cloud-controller-manager/Chart.yaml | 2 +- .../talos-cloud-controller-manager/README.md | 3 +- .../templates/configmap.yaml | 4 + .../values.yaml | 17 ++ .../cloud-controller-manager-daemonset.yml | 12 +- docs/deploy/cloud-controller-manager-edge.yml | 12 +- docs/deploy/cloud-controller-manager.yml | 12 +- docs/rules.yaml | 85 +++++++ go.mod | 52 ++--- go.sum | 122 +++++----- hack/ccm-config.yaml | 11 + pkg/nodeselector/selector.go | 123 ++++++++++ pkg/nodeselector/selector_test.go | 186 ++++++++++++++++ pkg/nodeselector/types.go | 66 ++++++ pkg/talos/cloud_config.go | 4 + pkg/talos/cloud_config_test.go | 17 +- pkg/talos/helper.go | 80 ++++++- pkg/talos/helper_test.go | 78 ++++++- pkg/talos/instances.go | 27 ++- pkg/transformer/transformer.go | 169 ++++++++++++++ pkg/transformer/transformer_test.go | 210 ++++++++++++++++++ 24 files changed, 1189 insertions(+), 141 deletions(-) create mode 100644 docs/rules.yaml create mode 100644 pkg/nodeselector/selector.go create mode 100644 pkg/nodeselector/selector_test.go create mode 100644 pkg/nodeselector/types.go create mode 100644 pkg/transformer/transformer.go create mode 100644 pkg/transformer/transformer_test.go diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 9445828..5d47899 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -35,10 +35,10 @@ jobs: go-version-file: 'go.mod' - name: Lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: - version: v1.56.2 - args: --config=.golangci.yml + version: v1.57.1 + args: --timeout=5m --config=.golangci.yml - name: Build timeout-minutes: 10 run: make images diff --git a/.github/workflows/release-charts.yaml b/.github/workflows/release-charts.yaml index 8163f5f..31532f1 100644 --- a/.github/workflows/release-charts.yaml +++ b/.github/workflows/release-charts.yaml @@ -24,8 +24,6 @@ jobs: - name: Install Helm uses: azure/setup-helm@v4 - with: - version: v3.12.2 - name: Install Cosign uses: sigstore/cosign-installer@v3.5.0 diff --git a/.golangci.yml b/.golangci.yml index 05e7fa9..66bb42f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,6 @@ # options for analysis running run: - go: '1.21' # default concurrency is a available CPU number # concurrency: 4 @@ -20,7 +19,8 @@ run: # won't be reported. Default value is empty list, but there is # no need to include all autogenerated files, we confidently recognize # autogenerated files. If it's not please let us know. - skip-files: + exclude-files: + - charts/ - docs/ # list of build tags, all linters use it. Default is empty list. @@ -34,7 +34,13 @@ run: # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" - format: line-number + formats: + - format: line-number + path: stdout + print-issued-lines: true + print-linter-name: true + uniq-by-line: true + sort-results: true # all available settings of specific linters linters-settings: @@ -46,15 +52,13 @@ linters-settings: # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; # default is false: such cases aren't reported by default. check-blank: true - govet: - # report about shadowed variables - check-shadowing: true + govet: {} gofmt: # simplify code: gofmt with `-s` option, true by default simplify: true gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 maligned: # print struct with more effective memory layout or not, false by default suggest-new: true @@ -66,6 +70,8 @@ linters-settings: min-len: 3 # minimal occurrences count to trigger, 3 by default min-occurrences: 3 + depguard: + list-type: blacklist misspell: # Correct spellings using locale preferences for US or UK. # Default is to use a neutral variety of English. @@ -167,11 +173,21 @@ linters: - wrapcheck - perfsprint + # temporarily disabled linters + - copyloopvar + # https://github.com/golangci/golangci-lint/issues/4606 + - intrange + # abandoned linters for which golangci shows the warning that the repo is archived by the owner - golint - interfacer - maligned - scopelint + - varcheck + - structcheck + - deadcode + - ifshort + - perfsprint disable-all: false fast: false diff --git a/charts/talos-cloud-controller-manager/Chart.yaml b/charts/talos-cloud-controller-manager/Chart.yaml index b5432b9..200a267 100644 --- a/charts/talos-cloud-controller-manager/Chart.yaml +++ b/charts/talos-cloud-controller-manager/Chart.yaml @@ -11,5 +11,5 @@ keywords: maintainers: - name: sergelogvinov url: https://github.com/sergelogvinov -version: 0.2.3 +version: 0.3.0 appVersion: "v1.6.0" diff --git a/charts/talos-cloud-controller-manager/README.md b/charts/talos-cloud-controller-manager/README.md index 037820d..93e7c99 100644 --- a/charts/talos-cloud-controller-manager/README.md +++ b/charts/talos-cloud-controller-manager/README.md @@ -1,6 +1,6 @@ # talos-cloud-controller-manager -![Version: 0.2.3](https://img.shields.io/badge/Version-0.2.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.6.0](https://img.shields.io/badge/AppVersion-v1.6.0-informational?style=flat-square) +![Version: 0.3.0](https://img.shields.io/badge/Version-0.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.6.0](https://img.shields.io/badge/AppVersion-v1.6.0-informational?style=flat-square) Talos Cloud Controller Manager Helm Chart @@ -79,5 +79,6 @@ helm upgrade -i --namespace=kube-system -f talos-ccm.yaml \ | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template. | | tolerations | list | `[{"effect":"NoSchedule","key":"node-role.kubernetes.io/control-plane","operator":"Exists"},{"effect":"NoSchedule","key":"node.cloudprovider.kubernetes.io/uninitialized","operator":"Exists"}]` | Tolerations for data pods assignment. ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ | +| transformations | list | `[]` | List of node transformations. Available matchExpressions key values: https://github.com/siderolabs/talos/blob/main/pkg/machinery/resources/runtime/platform_metadata.go#L28 | | updateStrategy | object | `{"rollingUpdate":{"maxUnavailable":1},"type":"RollingUpdate"}` | Deployment update stategy type. ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#updating-a-deployment | | useDaemonSet | bool | `false` | Deploy CCM in Daemonset mode. CCM will use hostNetwork and current node to access kubernetes/talos API You can run it without CNI plugin. | diff --git a/charts/talos-cloud-controller-manager/templates/configmap.yaml b/charts/talos-cloud-controller-manager/templates/configmap.yaml index 22a66c0..d9e6dfa 100644 --- a/charts/talos-cloud-controller-manager/templates/configmap.yaml +++ b/charts/talos-cloud-controller-manager/templates/configmap.yaml @@ -11,3 +11,7 @@ data: {{- if .Values.features.approveNodeCSR }} approveNodeCSR: true {{- end }} + {{- with .Values.transformations }} + transformations: + {{- toYaml . | nindent 6 }} + {{- end }} diff --git a/charts/talos-cloud-controller-manager/values.yaml b/charts/talos-cloud-controller-manager/values.yaml index 4afeb03..7150b91 100644 --- a/charts/talos-cloud-controller-manager/values.yaml +++ b/charts/talos-cloud-controller-manager/values.yaml @@ -40,6 +40,23 @@ features: # `approveNodeCSR` - check and approve node CSR. approveNodeCSR: true +# -- List of node transformations. +# Available matchExpressions key values: https://github.com/siderolabs/talos/blob/main/pkg/machinery/resources/runtime/platform_metadata.go#L28 +transformations: [] + # - name: control-plane region + # nodeSelector: + # - matchExpressions: + # - key: hostname + # operator: Regexp + # values: + # - ^control-.+$ + # annotations: + # talos.dev/instance-id: "{{ .InstanceID }}" + # labels: + # talos.dev/instance-spot: "{{ .Spot }}" + # platformMetadata: + # Region: "us-west-2" + # -- Log verbosity level. See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md # for description of individual verbosity levels. logVerbosityLevel: 2 diff --git a/docs/deploy/cloud-controller-manager-daemonset.yml b/docs/deploy/cloud-controller-manager-daemonset.yml index a85d38b..a125f95 100644 --- a/docs/deploy/cloud-controller-manager-daemonset.yml +++ b/docs/deploy/cloud-controller-manager-daemonset.yml @@ -5,7 +5,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -18,7 +18,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager-talos-secrets labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -34,7 +34,7 @@ kind: ConfigMap metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -51,7 +51,7 @@ kind: ClusterRole metadata: name: system:talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -159,7 +159,7 @@ kind: Service metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -183,7 +183,7 @@ kind: DaemonSet metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" diff --git a/docs/deploy/cloud-controller-manager-edge.yml b/docs/deploy/cloud-controller-manager-edge.yml index 6df4aab..f832efb 100644 --- a/docs/deploy/cloud-controller-manager-edge.yml +++ b/docs/deploy/cloud-controller-manager-edge.yml @@ -5,7 +5,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -18,7 +18,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager-talos-secrets labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -34,7 +34,7 @@ kind: ConfigMap metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -51,7 +51,7 @@ kind: ClusterRole metadata: name: system:talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -159,7 +159,7 @@ kind: Service metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -183,7 +183,7 @@ kind: Deployment metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" diff --git a/docs/deploy/cloud-controller-manager.yml b/docs/deploy/cloud-controller-manager.yml index 8ee1f3c..6b61731 100644 --- a/docs/deploy/cloud-controller-manager.yml +++ b/docs/deploy/cloud-controller-manager.yml @@ -5,7 +5,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -18,7 +18,7 @@ kind: ServiceAccount metadata: name: talos-cloud-controller-manager-talos-secrets labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -34,7 +34,7 @@ kind: ConfigMap metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -51,7 +51,7 @@ kind: ClusterRole metadata: name: system:talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -159,7 +159,7 @@ kind: Service metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" @@ -183,7 +183,7 @@ kind: Deployment metadata: name: talos-cloud-controller-manager labels: - helm.sh/chart: talos-cloud-controller-manager-0.2.3 + helm.sh/chart: talos-cloud-controller-manager-0.3.0 app.kubernetes.io/name: talos-cloud-controller-manager app.kubernetes.io/instance: talos-cloud-controller-manager app.kubernetes.io/version: "v1.6.0" diff --git a/docs/rules.yaml b/docs/rules.yaml new file mode 100644 index 0000000..037d468 --- /dev/null +++ b/docs/rules.yaml @@ -0,0 +1,85 @@ + # transformation rules for the node + transformations: + - name: oracle + # Match rules similar as kubernetes matchExpressions + nodeSelector: + - name: oracle vm in frankfurt + matchExpressions: + - key: platform + operator: In + values: + - oracle + - key: region + operator: In + values: + - eu-frankfurt-1 + # or + - name: special vm name + matchExpressions: + - key: hostname + operator: Regexp + values: + - ^web-oci-ad2-.+$ + + # We will annotate and label the nodes + annotations: + oci.oraclecloud.com/instance-id: "{{ .InstanceID }}" + oci.oraclecloud.com/compartment-id: ocid1.compartment.oc1..aaaaaaaabbbbbbccccccdddddd + labels: + talos.dev/zone": "frankfurt" + node-role.kubernetes.io/web: "" + + # Transforme the platformMetadata + platformMetadata: + zone: EU-FRANKFURT-1-AD-2 + + - name: openstack + nodeSelector: + - name: fix openstack zone + matchExpressions: + - key: platform + operator: In + values: + - openstack + - key: zone + operator: In + values: + - nova + - key: hostname + operator: Regexp + values: + - ^web-fra-.+$ + annotations: {} + labels: + talos.dev/zone: "frankfurt" + node-role.kubernetes.io/web: "" + + # Set providerId/region/zone based on the hostname and platform + platformMetadata: + zone: frankfurt + region: eu-frankfurt-1 + providerId: openstack://fra/{{ .InstanceID }} + + # Special features for the node + features: + publicIPPredict: true + +--- +# Metadata for the node + +# Oracle +platform: oracle +hostname: web-oci-ad2-1 +region: eu-frankfurt-1 +zone: EU-FRANKFURT-1-AD-2 +instanceType: VM.Standard.A1.Flex +instanceId: ocid1.instance.oc1.eu-frankfurt-1.aaaaaaaabbbbbbccccccdddddd +providerId: oci://ocid1.instance.oc1.eu-frankfurt-1.aaaaaaaabbbbbbccccccdddddd + +# Openstack +platform: openstack +hostname: web-fra-de11 +zone: nova +instanceType: b2-7 +instanceId: 9a1fd9ee-9f05-473e-88d8-1b1b1b1b1b1b +providerId: openstack:///9a1fd9ee-9f05-473e-88d8-1b1b1b1b1b1b diff --git a/go.mod b/go.mod index af516c1..1e3fdd6 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,24 @@ module github.com/siderolabs/talos-cloud-controller-manager go 1.22.2 require ( - github.com/cosi-project/runtime v0.4.1 + github.com/cosi-project/runtime v0.4.2 github.com/siderolabs/net v0.4.0 - github.com/siderolabs/talos/pkg/machinery v1.6.7 + github.com/siderolabs/talos/pkg/machinery v1.7.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 - k8s.io/api v0.29.3 - k8s.io/apimachinery v0.29.3 - k8s.io/client-go v0.29.3 - k8s.io/cloud-provider v0.29.3 - k8s.io/component-base v0.29.3 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 + k8s.io/cloud-provider v0.30.0 + k8s.io/component-base v0.30.0 k8s.io/klog/v2 v2.120.1 - k8s.io/utils v0.0.0-20240310230437-4693a0247e57 + k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 ) require ( - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect @@ -41,7 +41,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gertd/go-pluralize v0.2.1 // indirect @@ -53,7 +53,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.17.7 // indirect + github.com/google/cel-go v0.17.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -66,21 +66,22 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/jsimonetti/rtnetlink v1.4.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mdlayher/ethtool v0.1.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/socket v0.5.1 // 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.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect @@ -110,29 +111,28 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiserver v0.29.3 // indirect - k8s.io/component-helpers v0.29.3 // indirect - k8s.io/controller-manager v0.29.3 // indirect - k8s.io/kms v0.29.3 // indirect - k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect + k8s.io/apiserver v0.30.0 // indirect + k8s.io/component-helpers v0.30.0 // indirect + k8s.io/controller-manager v0.30.0 // indirect + k8s.io/kms v0.30.0 // indirect + k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7cf292e..73d9a8b 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiV cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= @@ -47,8 +47,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cosi-project/runtime v0.4.1 h1:9lJWw5cl3Lz1qP32bl2vxAsJs6LM8KdUGLCc9t/EGqw= -github.com/cosi-project/runtime v0.4.1/go.mod h1:eXVAHf9QzzSVblLUtHHPFOZ7JBuz+GypHbao1vw+SdQ= +github.com/cosi-project/runtime v0.4.2 h1:kJkhorzDWierDDbXn1BDHS6iQ7ai9AdvQOnK5uG/g8g= +github.com/cosi-project/runtime v0.4.2/go.mod h1:eXVAHf9QzzSVblLUtHHPFOZ7JBuz+GypHbao1vw+SdQ= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -62,8 +62,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= -github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= -github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -77,8 +77,8 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/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-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -95,25 +95,22 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= -github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto= +github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f h1:f00RU+zOX+B3rLAmMMkzHUF2h1z4DeYR9tTCvEq2REY= +github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -141,8 +138,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= -github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/jsimonetti/rtnetlink v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE= +github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w= 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= @@ -164,8 +161,8 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5 github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= -github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 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= @@ -179,12 +176,14 @@ github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8 github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA= +github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= @@ -216,8 +215,8 @@ github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I= github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM= github.com/siderolabs/protoenc v0.2.1 h1:BqxEmeWQeMpNP3R6WrPqDatX8sM/r4t97OP8mFmg6GA= github.com/siderolabs/protoenc v0.2.1/go.mod h1:StTHxjet1g11GpNAWiATgc8K0HMKiFSEVVFOa/H0otc= -github.com/siderolabs/talos/pkg/machinery v1.6.7 h1:L/IV0+w+0n6Jsxa1LEFOxPaNNluGrZqn3YRvAXk/xzA= -github.com/siderolabs/talos/pkg/machinery v1.6.7/go.mod h1:X1rb+ZA0tRjclzuH4Kurune4DK6kr58B/vorQ88KwpE= +github.com/siderolabs/talos/pkg/machinery v1.7.1 h1:sVFQ0lNE6+kOomSZA8iuktzG1A4zSW9KTsB2TLaTPsU= +github.com/siderolabs/talos/pkg/machinery v1.7.1/go.mod h1:YBl9KDCD45Uc7N0rXBY1JqovUn1n46ekUPSNbEVZzQU= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -297,8 +296,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= -golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -314,8 +313,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -350,7 +349,6 @@ golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -364,24 +362,20 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 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/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38= +google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -399,35 +393,35 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= -k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= -k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= -k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= -k8s.io/apiserver v0.29.3 h1:xR7ELlJ/BZSr2n4CnD3lfA4gzFivh0wwfNfz9L0WZcE= -k8s.io/apiserver v0.29.3/go.mod h1:hrvXlwfRulbMbBgmWRQlFru2b/JySDpmzvQwwk4GUOs= -k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= -k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= -k8s.io/cloud-provider v0.29.3 h1:y39hNq0lrPD1qmqQ2ykwMJGeWF9LsepVkR2a4wskwLc= -k8s.io/cloud-provider v0.29.3/go.mod h1:daDV1WkAO6pTrdsn7v8TpN/q9n75ExUC4RJDl7vlPKk= -k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= -k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= -k8s.io/component-helpers v0.29.3 h1:1dqZswuZgT2ZMixYeORyCUOAApXxgsvjVSgfoUT+P4o= -k8s.io/component-helpers v0.29.3/go.mod h1:yiDqbRQrnQY+sPju/bL7EkwDJb6LVOots53uZNMZBos= -k8s.io/controller-manager v0.29.3 h1:pvm3mirypgW7kM6dHRk6O5ANZj4bZTWirfk5gO6RlCo= -k8s.io/controller-manager v0.29.3/go.mod h1:RNxpf0d1WAo59sOLd32isWJP0oZ7Zxr+q4VEEaSq4gk= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apiserver v0.30.0 h1:QCec+U72tMQ+9tR6A0sMBB5Vh6ImCEkoKkTDRABWq6M= +k8s.io/apiserver v0.30.0/go.mod h1:smOIBq8t0MbKZi7O7SyIpjPsiKJ8qa+llcFCluKyqiY= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/cloud-provider v0.30.0 h1:hz1MXkFjsyO167sRZVchXEi2YYMQ6kolBi79nuICjzw= +k8s.io/cloud-provider v0.30.0/go.mod h1:iyVcGvDfmZ7m5cliI9TTHj0VTjYDNpc/K71Gp6hukjU= +k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o= +k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ= +k8s.io/component-helpers v0.30.0 h1:xbJtNCfSM4SB/Tz5JqCKDZv4eT5LVi/AWQ1VOxhmStU= +k8s.io/component-helpers v0.30.0/go.mod h1:68HlSwXIumMKmCx8cZe1PoafQEYh581/sEpxMrkhmX4= +k8s.io/controller-manager v0.30.0 h1:jqqT8cK0Awdy0IfT0yuqYIRmwskbdzH5AEZqkuhEVMs= +k8s.io/controller-manager v0.30.0/go.mod h1:suM1r/pxUuk2ij5Bbm7W9kBLrFujXuzIboNuWK5AfRA= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.29.3 h1:ReljsAUhYlm2spdT4yXmY+9a8x8dc/OT4mXvwQPPteQ= -k8s.io/kms v0.29.3/go.mod h1:TBGbJKpRUMk59neTMDMddjIDL+D4HuFUbpuiuzmOPg0= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= +k8s.io/kms v0.30.0 h1:ZlnD/ei5lpvUlPw6eLfVvH7d8i9qZ6HwUQgydNVks8g= +k8s.io/kms v0.30.0/go.mod h1:GrMurD0qk3G4yNgGcsCEmepqf9KyyIrTXYR2lyUOJC4= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 h1:ao5hUqGhsqdm+bYbjH/pRkCs0unBGe9UyDahzs9zQzQ= +k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/ccm-config.yaml b/hack/ccm-config.yaml index 3be7656..73b0bc3 100644 --- a/hack/ccm-config.yaml +++ b/hack/ccm-config.yaml @@ -4,3 +4,14 @@ global: # endpoints: # - 1.2.3.4 # - 4.3.2.1 +transformations: + - nodeSelector: + - matchExpressions: + - key: hostname + operator: Regexp + values: + - ^web-.+$ + annotations: + talos.dev/instance-id: "id-{{ .InstanceID }}" + labels: + node-role.kubernetes.io/web: "" diff --git a/pkg/nodeselector/selector.go b/pkg/nodeselector/selector.go new file mode 100644 index 0000000..9a62ff0 --- /dev/null +++ b/pkg/nodeselector/selector.go @@ -0,0 +1,123 @@ +// Package nodeselector provides a mechanism to match node based on node selector rules. +package nodeselector + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" +) + +// Match returns true if the node metadata matches the node selector rules. +func Match(rules []NodeSelectorTerm, fields map[string]string) (bool, error) { + if len(rules) == 0 { + return true, nil + } + + for _, rule := range rules { + match, err := MatchExpressions(rule.MatchExpressions, fields) + if err != nil { + return false, err + } + + if match { + return true, nil + } + } + + return false, nil +} + +// MatchExpressions returns true if the node metadata matches the node selector expressions. +// +//nolint:cyclop,gocyclo +func MatchExpressions(rules []NodeSelectorRequirement, fields map[string]string) (bool, error) { + if len(rules) == 0 { + return false, nil + } + + matchs := make([]bool, len(rules)) + + for idx, rule := range rules { + switch rule.Operator { + case NodeSelectorOpIn: + if len(rule.Values) == 0 { + return false, fmt.Errorf("values must be non-empty for operator '%s'", rule.Operator) + } + + if value, ok := fields[strings.ToLower(rule.Key)]; ok { + matchs[idx] = slices.Contains(rule.Values, value) + } + + case NodeSelectorOpNotIn: + if len(rule.Values) == 0 { + return false, fmt.Errorf("values must be non-empty for operator '%s'", rule.Operator) + } + + if value, ok := fields[strings.ToLower(rule.Key)]; ok { + matchs[idx] = !slices.Contains(rule.Values, value) + } + + case NodeSelectorOpExists: + if len(rule.Values) > 0 { + return false, fmt.Errorf("values must be empty for operator %s", rule.Operator) + } + + if _, ok := fields[strings.ToLower(rule.Key)]; ok { + matchs[idx] = true + } + + case NodeSelectorOpDoesNotExist: + if len(rule.Values) > 0 { + return false, fmt.Errorf("values must be empty for operator %s", rule.Operator) + } + + if _, ok := fields[strings.ToLower(rule.Key)]; !ok { + matchs[idx] = true + } + + case NodeSelectorOpGt, NodeSelectorOpLt: + if len(rule.Values) != 1 { + return false, fmt.Errorf("values must have a single element for operator %s", rule.Operator) + } + + if value, ok := fields[strings.ToLower(rule.Key)]; ok { + lsValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return false, fmt.Errorf("failed to parse value %s as int", value) + } + + rValue, err := strconv.ParseInt(rule.Values[0], 10, 64) + if err != nil { + return false, fmt.Errorf("failed to parse value %s as int", rule.Values[0]) + } + + matchs[idx] = (rule.Operator == NodeSelectorOpGt && lsValue > rValue) || (rule.Operator == NodeSelectorOpLt && lsValue < rValue) + } + + case NodeSelectorOpRegexp: + if len(rule.Values) != 1 { + return false, fmt.Errorf("values must have a single element for operator %s", rule.Operator) + } + + if value, ok := fields[strings.ToLower(rule.Key)]; ok { + r := regexp.MustCompile(rule.Values[0]) + + matchs[idx] = r.MatchString(value) + } + + default: + return false, fmt.Errorf("%s not a valid selector operator", rule.Operator) + } + } + + // And operation between all matchs + for _, match := range matchs { + if !match { + return false, nil + } + } + + return true, nil +} diff --git a/pkg/nodeselector/selector_test.go b/pkg/nodeselector/selector_test.go new file mode 100644 index 0000000..a8d8296 --- /dev/null +++ b/pkg/nodeselector/selector_test.go @@ -0,0 +1,186 @@ +package nodeselector_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/talos-cloud-controller-manager/pkg/nodeselector" +) + +func TestMatch(t *testing.T) { + fields := map[string]string{ + "platform": "test-platform", + "hostname": "test-hostname", + "region": "region-a", + "int": "10", + } + + for _, tt := range []struct { + name string + rules []nodeselector.NodeSelectorTerm + fields map[string]string + expected bool + expectedError error + }{ + { + name: "NotMatch with empty rules", + rules: []nodeselector.NodeSelectorTerm{}, + fields: fields, + expected: true, + }, + { + name: "NotMatch with empty expression rules", + rules: []nodeselector.NodeSelectorTerm{ + { + MatchExpressions: []nodeselector.NodeSelectorRequirement{}, + }, + }, + fields: fields, + expected: false, + }, + { + name: "NotMatch with nonexistent platform", + rules: []nodeselector.NodeSelectorTerm{ + { + MatchExpressions: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{"bad-platform"}, + }, + }, + }, + }, + fields: fields, + expected: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + match, err := nodeselector.Match(tt.rules, tt.fields) + + if tt.expectedError != nil { + assert.NotNil(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, match) + } + }) + } +} + +func TestMatchExpressions(t *testing.T) { + fields := map[string]string{ + "platform": "test-platform", + "hostname": "test-hostname", + "region": "region-a", + "int": "10", + } + + for _, tt := range []struct { + name string + rules []nodeselector.NodeSelectorRequirement + fields map[string]string + expected bool + expectedError error + }{ + { + name: "MatchExpressions with In operator error", + rules: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{}, + }, + }, + fields: fields, + expected: false, + expectedError: fmt.Errorf("values must be non-empty for operator 'In'"), + }, + { + name: "MatchExpressions with In and Exists operator", + rules: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{"test-platform"}, + }, + { + Key: "region", + Operator: nodeselector.NodeSelectorOpExists, + }, + { + Key: "fake-key", + Operator: nodeselector.NodeSelectorOpDoesNotExist, + }, + }, + fields: fields, + expected: true, + }, + { + name: "MatchExpressions with GtLt operator", + rules: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{"test-platform"}, + }, + { + Key: "int", + Operator: nodeselector.NodeSelectorOpGt, + Values: []string{"5"}, + }, + }, + fields: fields, + expected: true, + }, + { + name: "MatchExpressions with regexp operator", + rules: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{"test-platform"}, + }, + { + Key: "hostname", + Operator: nodeselector.NodeSelectorOpRegexp, + Values: []string{"^test.+$"}, + }, + }, + fields: fields, + expected: true, + }, + { + name: "MatchExpressions with regexp operator did not match", + rules: []nodeselector.NodeSelectorRequirement{ + { + Key: "platform", + Operator: nodeselector.NodeSelectorOpIn, + Values: []string{"test-platform"}, + }, + { + Key: "hostname", + Operator: nodeselector.NodeSelectorOpRegexp, + Values: []string{"^somename.+$"}, + }, + }, + fields: fields, + expected: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + match, err := nodeselector.MatchExpressions(tt.rules, tt.fields) + + if tt.expectedError != nil { + assert.NotNil(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, match) + } + }) + } +} diff --git a/pkg/nodeselector/types.go b/pkg/nodeselector/types.go new file mode 100644 index 0000000..900b587 --- /dev/null +++ b/pkg/nodeselector/types.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodeselector + +// Source(04/2024): https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/core/types.go with modifications (NodeSelectorOpRegexp) + +// NodeSelectorTerm represents expressions and fields required to select nodes. +// A null or empty node selector term matches no objects. The requirements of +// them are ANDed. +// The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. +// +//nolint:revive +type NodeSelectorTerm struct { + // A list of node selector requirements by node's labels. + MatchExpressions []NodeSelectorRequirement `yaml:"matchExpressions,omitempty"` +} + +// NodeSelectorRequirement is a selector that contains values, a key, and an operator +// that relates the key and values. +// +//nolint:revive +type NodeSelectorRequirement struct { + // The label key that the selector applies to. + Key string `yaml:"key,omitempty"` + // Represents a key's relationship to a set of values. + // Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + Operator NodeSelectorOperator `yaml:"operator,omitempty"` + // An array of string values. If the operator is In or NotIn, + // the values array must be non-empty. If the operator is Exists or DoesNotExist, + // the values array must be empty. If the operator is Gt or Lt, the values + // array must have a single element, which will be interpreted as an integer. + // This array is replaced during a strategic merge patch. + // +optional + Values []string `yaml:"values,omitempty"` +} + +// NodeSelectorOperator is the set of operators that can be used in +// a node selector requirement. +// +//nolint:revive +type NodeSelectorOperator string + +// These are valid values of NodeSelectorOperator. +const ( + NodeSelectorOpIn NodeSelectorOperator = "In" + NodeSelectorOpNotIn NodeSelectorOperator = "NotIn" + NodeSelectorOpExists NodeSelectorOperator = "Exists" + NodeSelectorOpDoesNotExist NodeSelectorOperator = "DoesNotExist" + NodeSelectorOpGt NodeSelectorOperator = "Gt" + NodeSelectorOpLt NodeSelectorOperator = "Lt" + NodeSelectorOpRegexp NodeSelectorOperator = "Regexp" +) diff --git a/pkg/talos/cloud_config.go b/pkg/talos/cloud_config.go index 29d4aac..bf3c3ef 100644 --- a/pkg/talos/cloud_config.go +++ b/pkg/talos/cloud_config.go @@ -7,12 +7,16 @@ import ( yaml "gopkg.in/yaml.v3" + "github.com/siderolabs/talos-cloud-controller-manager/pkg/transformer" + "k8s.io/klog/v2" ) type cloudConfig struct { // Global configuration. Global cloudConfigGlobal `yaml:"global,omitempty"` + // Node transformation configuration. + Transformations []transformer.NodeTerm `yaml:"transformations,omitempty"` } type cloudConfigGlobal struct { diff --git a/pkg/talos/cloud_config_test.go b/pkg/talos/cloud_config_test.go index 03754c0..921cc92 100644 --- a/pkg/talos/cloud_config_test.go +++ b/pkg/talos/cloud_config_test.go @@ -29,8 +29,21 @@ func TestReadCloudConfig(t *testing.T) { cfg, err := readCloudConfig(strings.NewReader(` global: - approveNodeCSR: true - preferIPv6: true + approveNodeCSR: true + preferIPv6: true +transformations: +- name: cluster + nodeSelector: + - name: cluter-1 + matchExpressions: + - key: platform + operator: In + values: + - cluter + annotations: + cluster-platform: "{{ .Platform }}" + labels: + node-role.kubernetes.io/web: "" `)) if err != nil { t.Fatalf("Should succeed when a valid config is provided: %s", err) diff --git a/pkg/talos/helper.go b/pkg/talos/helper.go index 0d21b49..9d0eebd 100644 --- a/pkg/talos/helper.go +++ b/pkg/talos/helper.go @@ -3,6 +3,7 @@ package talos import ( "context" "crypto/x509" + "encoding/json" "fmt" "strings" @@ -12,6 +13,8 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" clientkubernetes "k8s.io/client-go/kubernetes" cloudproviderapi "k8s.io/cloud-provider/api" cloudnodeutil "k8s.io/cloud-provider/node/helpers" @@ -72,24 +75,79 @@ func getNodeAddresses(config *cloudConfig, platform string, nodeIPs []string, if return addresses } -func syncNodeLabels(c *client, node *v1.Node, meta *runtime.PlatformMetadataSpec) error { - nodeLabels := node.ObjectMeta.Labels - labelsToUpdate := map[string]string{} +func syncNodeAnnotations(ctx context.Context, c *client, node *v1.Node, nodeAnnotations map[string]string) error { + nodeAnnotationsOrig := node.ObjectMeta.Labels + annotationsToUpdate := map[string]string{} + + for k, v := range nodeAnnotations { + if r, ok := nodeAnnotationsOrig[k]; !ok || r != v { + annotationsToUpdate[k] = v + } + } + + if len(annotationsToUpdate) > 0 { + oldData, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal the existing node %#v: %w", node, err) + } + + newNode := node.DeepCopy() + if newNode.Annotations == nil { + newNode.Annotations = make(map[string]string) + } + + for k, v := range annotationsToUpdate { + newNode.Annotations[k] = v + } + + newData, err := json.Marshal(newNode) + if err != nil { + return fmt.Errorf("failed to marshal the new node %#v: %w", newNode, err) + } + + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, &v1.Node{}) + if err != nil { + return fmt.Errorf("failed to create a two-way merge patch: %v", err) + } - if nodeLabels == nil { - nodeLabels = map[string]string{} + if _, err := c.kclient.CoreV1().Nodes().Patch(ctx, node.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { + return fmt.Errorf("failed to patch the node: %v", err) + } + } + + return nil +} + +func setTalosNodeLabels(c *client, meta *runtime.PlatformMetadataSpec) map[string]string { + if meta == nil { + return make(map[string]string) + } + + labels := make(map[string]string, 3) + + if meta.Platform != "" { + labels[ClusterNodePlatformLabel] = meta.Platform } - if meta.Platform != "" && nodeLabels[ClusterNodePlatformLabel] != meta.Platform { - labelsToUpdate[ClusterNodePlatformLabel] = meta.Platform + if meta.Spot { + labels[ClusterNodeLifeCycleLabel] = "spot" } - if meta.Spot && nodeLabels[ClusterNodeLifeCycleLabel] != "spot" { - labelsToUpdate[ClusterNodeLifeCycleLabel] = "spot" + if clusterName := c.talos.GetClusterName(); clusterName != "" { + labels[ClusterNameNodeLabel] = clusterName } - if clusterName := c.talos.GetClusterName(); clusterName != "" && nodeLabels[ClusterNameNodeLabel] != clusterName { - labelsToUpdate[ClusterNameNodeLabel] = clusterName + return labels +} + +func syncNodeLabels(c *client, node *v1.Node, nodeLabels map[string]string) error { + nodeLabelsOrig := node.ObjectMeta.Labels + labelsToUpdate := map[string]string{} + + for k, v := range nodeLabels { + if r, ok := nodeLabelsOrig[k]; !ok || r != v { + labelsToUpdate[k] = v + } } if len(labelsToUpdate) > 0 { diff --git a/pkg/talos/helper_test.go b/pkg/talos/helper_test.go index 81861ba..73bd8a2 100644 --- a/pkg/talos/helper_test.go +++ b/pkg/talos/helper_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "fmt" + "maps" "net" "net/netip" "strings" @@ -11,6 +12,8 @@ import ( "github.com/stretchr/testify/assert" + "github.com/siderolabs/talos-cloud-controller-manager/pkg/nodeselector" + "github.com/siderolabs/talos-cloud-controller-manager/pkg/transformer" "github.com/siderolabs/talos/pkg/machinery/resources/network" "github.com/siderolabs/talos/pkg/machinery/resources/runtime" @@ -151,10 +154,30 @@ func TestGetNodeAddresses(t *testing.T) { func TestSyncNodeLabels(t *testing.T) { t.Setenv("TALOSCONFIG", "../../hack/talosconfig") - cfg := cloudConfig{Global: cloudConfigGlobal{ - ClusterName: "test-cluster", - Endpoints: []string{"127.0.0.1"}, - }} + cfg := cloudConfig{ + Global: cloudConfigGlobal{ + ClusterName: "test-cluster", + Endpoints: []string{"127.0.0.1"}, + }, + Transformations: []transformer.NodeTerm{ + { + NodeSelector: []nodeselector.NodeSelectorTerm{ + { + MatchExpressions: []nodeselector.NodeSelectorRequirement{ + { + Key: "Hostname", + Operator: "Regexp", + Values: []string{"^web-.+$"}, + }, + }, + }, + }, + Labels: map[string]string{ + "node-role.kubernetes.io/web": "", + }, + }, + }, + } ctx := context.Background() nodes := &v1.NodeList{ Items: []v1.Node{ @@ -167,6 +190,15 @@ func TestSyncNodeLabels(t *testing.T) { Name: "node1", }, }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "web-1", + }, + }, }, } @@ -258,9 +290,45 @@ func TestSyncNodeLabels(t *testing.T) { }, }, }, + { + name: "node with custom labels", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "web-1", + }, + }, + meta: &runtime.PlatformMetadataSpec{ + Platform: "nocloud", + Hostname: "web-1", + }, + expectedError: nil, + expectedNode: &v1.Node{ + TypeMeta: metav1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "web-1", + Labels: map[string]string{ + ClusterNameNodeLabel: "test-cluster", + ClusterNodePlatformLabel: "nocloud", + "node-role.kubernetes.io/web": "", + }, + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { - err := syncNodeLabels(client, tt.node, tt.meta) + nodeSpec, err := transformer.TransformNode(client.config.Transformations, tt.meta) + assert.NoError(t, err) + + labels := setTalosNodeLabels(client, tt.meta) + + if nodeSpec != nil && nodeSpec.Labels != nil { + maps.Copy(labels, nodeSpec.Labels) + } + + err = syncNodeLabels(client, tt.node, labels) assert.Equal(t, tt.expectedError, err) diff --git a/pkg/talos/instances.go b/pkg/talos/instances.go index 035847c..821e03b 100644 --- a/pkg/talos/instances.go +++ b/pkg/talos/instances.go @@ -3,8 +3,10 @@ package talos import ( "context" "fmt" + "maps" "strings" + "github.com/siderolabs/talos-cloud-controller-manager/pkg/transformer" "github.com/siderolabs/talos-cloud-controller-manager/pkg/utils/net" "github.com/siderolabs/talos-cloud-controller-manager/pkg/utils/platform" "github.com/siderolabs/talos/pkg/machinery/resources/runtime" @@ -44,6 +46,8 @@ func (i *instances) InstanceShutdown(_ context.Context, node *v1.Node) (bool, er // InstanceMetadata returns the instance's metadata. The values returned in InstanceMetadata are // translated into specific fields in the Node object on registration. // Use the node.name or node.spec.providerID field to find the node in the cloud provider. +// +//nolint:gocyclo,cyclop func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) { klog.V(4).Info("instances.InstanceMetadata() called, node: ", node.Name) @@ -90,6 +94,11 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud } } + nodeSpec, err := transformer.TransformNode(i.c.config.Transformations, meta) + if err != nil { + return nil, fmt.Errorf("error transforming node: %w", err) + } + ifaces, err := i.c.getNodeIfaces(ctx, nodeIP) if err != nil { return nil, fmt.Errorf("error getting interfaces list from the node %s: %w", node.Name, err) @@ -112,7 +121,23 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud }, nil } - if err := syncNodeLabels(i.c, node, meta); err != nil { + if nodeSpec != nil && nodeSpec.Annotations != nil { + klog.V(4).Infof("instances.InstanceMetadata() node %s has annotations: %+v", node.Name, nodeSpec.Annotations) + + if err := syncNodeAnnotations(ctx, i.c, node, nodeSpec.Annotations); err != nil { + klog.Errorf("failed update annotations for node %s, %v", node.Name, err) + } + } + + nodeLabels := setTalosNodeLabels(i.c, meta) + + if nodeSpec != nil && nodeSpec.Labels != nil { + klog.V(4).Infof("instances.InstanceMetadata() node %s has labels: %+v", node.Name, nodeSpec.Labels) + + maps.Copy(nodeLabels, nodeSpec.Labels) + } + + if err := syncNodeLabels(i.c, node, nodeLabels); err != nil { klog.Errorf("failed update labels for node %s, %v", node.Name, err) } diff --git a/pkg/transformer/transformer.go b/pkg/transformer/transformer.go new file mode 100644 index 0000000..fac8ac8 --- /dev/null +++ b/pkg/transformer/transformer.go @@ -0,0 +1,169 @@ +// Package transformer provides a mechanism to transform node specification based on node transformation rules. +package transformer + +import ( + "bytes" + "fmt" + "html/template" + "reflect" + "slices" + "strings" + + "github.com/siderolabs/talos-cloud-controller-manager/pkg/nodeselector" + "github.com/siderolabs/talos/pkg/machinery/resources/runtime" +) + +// NodeTerm represents expressions and fields to transform node metadata. +type NodeTerm struct { + Name string `yaml:"name,omitempty"` + NodeSelector []nodeselector.NodeSelectorTerm `yaml:"nodeSelector,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + PlatformMetadata map[string]string `yaml:"platformMetadata,omitempty"` +} + +// NodeSpec represents the transformed node specifcations. +type NodeSpec struct { + Annotations map[string]string + Labels map[string]string +} + +var prohibitedPlatformMetadataKeys = []string{"hostname", "platform"} + +// TransformNode transforms the node metadata based on the node transformation rules. +func TransformNode(terms []NodeTerm, platformMetadata *runtime.PlatformMetadataSpec) (*NodeSpec, error) { + if len(terms) == 0 { + return nil, nil + } + + metadata := metadataFromStruct(platformMetadata) + + for _, term := range terms { + match, err := nodeselector.Match(term.NodeSelector, metadata) + if err != nil { + return nil, err + } + + if match { + node := &NodeSpec{ + Annotations: make(map[string]string), + Labels: make(map[string]string), + } + + if term.Annotations != nil { + for k, v := range term.Annotations { + t, err := executeTemplate(v, platformMetadata) + if err != nil { + return nil, fmt.Errorf("failed to transformer annotation '%q': %w", k, err) + } + + node.Annotations[k] = t + } + } + + if term.Labels != nil { + for k, v := range term.Labels { + t, err := executeTemplate(v, platformMetadata) + if err != nil { + return nil, fmt.Errorf("failed to transformer label '%s': %w", k, err) + } + + node.Labels[k] = t + } + } + + if term.PlatformMetadata != nil { + p := reflect.ValueOf(platformMetadata) + ps := p.Elem() + + for k, v := range term.PlatformMetadata { + if slices.Contains(prohibitedPlatformMetadataKeys, strings.ToLower(k)) { + continue + } + + t, err := executeTemplate(v, platformMetadata) + if err != nil { + return nil, fmt.Errorf("failed to transformer platform metadata '%s': %w", k, err) + } + + f := ps.FieldByNameFunc(func(fieldName string) bool { + return strings.EqualFold(fieldName, k) + }) + + if f.IsValid() { + switch f.Kind() { //nolint:exhaustive + case reflect.Bool: + f.SetBool(t == "true") + case reflect.String: + f.SetString(strings.TrimSpace(t)) + default: + return nil, fmt.Errorf("unsupported platform metadata field %s", k) + } + } + } + } + + return node, nil + } + } + + return nil, nil +} + +func executeTemplate(tmpl string, data interface{}) (string, error) { + t, err := template.New("transformer").Parse(tmpl) + if err != nil { + return "", fmt.Errorf("failed to parse template %q: %w", tmpl, err) + } + + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} + +func metadataFromStruct(in *runtime.PlatformMetadataSpec) map[string]string { + if in == nil { + return nil + } + + metadata := make(map[string]string) + + v := reflect.ValueOf(in) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + // skip unexported fields + if !v.Field(i).CanInterface() { + continue + } + + tag := v.Type().Field(i).Tag.Get("yaml") + if tag == "" { + continue + } + + fieldName := strings.ToLower(strings.Split(tag, ",")[0]) + + reflectedValue := reflect.Indirect(v.Field(i)) + if reflectedValue.IsValid() { + switch reflectedValue.Kind() { //nolint:exhaustive + case reflect.Bool: + metadata[fieldName] = fmt.Sprintf("%t", reflectedValue.Bool()) + case reflect.String: + v := reflectedValue.String() + if v != "" { + metadata[fieldName] = v + } + } + } + } + } + + return metadata +} diff --git a/pkg/transformer/transformer_test.go b/pkg/transformer/transformer_test.go new file mode 100644 index 0000000..8ec4d49 --- /dev/null +++ b/pkg/transformer/transformer_test.go @@ -0,0 +1,210 @@ +package transformer_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/talos-cloud-controller-manager/pkg/transformer" + "github.com/siderolabs/talos/pkg/machinery/resources/runtime" +) + +func TestMatch(t *testing.T) { + for _, tt := range []struct { + name string + terms []transformer.NodeTerm + metadata runtime.PlatformMetadataSpec + expected *transformer.NodeSpec + expectedMeta *runtime.PlatformMetadataSpec + expectedError error + }{ + { + name: "Empty rules", + terms: []transformer.NodeTerm{}, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + }, + { + name: "Transform labels", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + Labels: map[string]string{ + "my-label-name": "my-value", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{}, + Labels: map[string]string{ + "my-label-name": "my-value", + }, + }, + }, + { + name: "Transform annotations and labels", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + Labels: map[string]string{ + "my-label-name": "my-value", + }, + Annotations: map[string]string{ + "my-annotation-name": "my-annotation-value", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{ + "my-annotation-name": "my-annotation-value", + }, + Labels: map[string]string{ + "my-label-name": "my-value", + }, + }, + }, + { + name: "Transform bad template", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + Labels: map[string]string{ + "label-template": "my-value-{{ .Spot", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expectedError: fmt.Errorf("failed to transformer label 'label-template': failed to parse template \"my-value-{{ .Spot\": template: transformer:1: unclosed action"), + }, + { + name: "Transform annotations with template", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + Annotations: map[string]string{ + "annotation-template": "my-value-{{ .Platform }}", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{ + "annotation-template": "my-value-test-platform", + }, + Labels: map[string]string{}, + }, + }, + { + name: "Transform labels with template and missing metadata fields", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + Labels: map[string]string{ + "label-template": "my-value-{{ .Spot }}-{{ .Zone }}", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{}, + Labels: map[string]string{ + "label-template": "my-value-false-", + }, + }, + }, + { + name: "Transform metadata fields", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + PlatformMetadata: map[string]string{ + "Spot": "true", + "Zone": "us-west1", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{}, + Labels: map[string]string{}, + }, + expectedMeta: &runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + Spot: true, + Zone: "us-west1", + }, + }, + { + name: "Transform metadata with wrong fields", + terms: []transformer.NodeTerm{ + { + Name: "my-transformer", + PlatformMetadata: map[string]string{ + "Hostname": "fake-hostname", + "spot": "true", + "zoNe": "us-west1", + "wrong": "value", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + Annotations: map[string]string{}, + Labels: map[string]string{}, + }, + expectedMeta: &runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + Spot: true, + Zone: "us-west1", + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + node, err := transformer.TransformNode(tt.terms, &tt.metadata) + + if tt.expectedError != nil { + assert.NotNil(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + + if tt.expected != nil { + assert.NotNil(t, node) + assert.EqualValues(t, tt.expected, node) + + if tt.expectedMeta != nil { + assert.EqualValues(t, tt.expectedMeta, &tt.metadata) + } + } + } + }) + } +}