From e1e340bdd910dc1f9c7fd08f71fb14352e96dbbf Mon Sep 17 00:00:00 2001 From: Philipp Sauter Date: Thu, 29 Sep 2022 12:22:36 +0200 Subject: [PATCH] feat: expose Talos node labels as a machine configuration field We add the `nodeLabels` key to the machine config to allow users to add node labels to the kubernetes Node object. A controller reads the nodeLabels from the machine config and applies them via the kubernetes API. Older versions of talosctl will throw an unknown keys error if `edit mc` is called on a node with this change. Fixes #6301 Signed-off-by: Philipp Sauter Signed-off-by: Andrey Smirnov --- api/resource/definitions/k8s/k8s.proto | 6 + go.mod | 1 + go.sum | 1 + hack/release.toml | 16 + .../address_filter.go} | 16 +- .../address_filter_test.go} | 6 +- .../control_plane.go} | 34 +- .../control_plane_test.go} | 8 +- .../pkg/controllers/k8s/node_label_spec.go | 103 ++++++ .../controllers/k8s/node_label_spec_test.go | 274 ++++++++++++++ .../pkg/controllers/k8s/node_labels_apply.go | 280 +++++++++++++++ .../controllers/k8s/node_labels_apply_test.go | 135 +++++++ .../pkg/runtime/v1alpha1/v1alpha1_runtime.go | 2 + .../runtime/v1alpha2/v1alpha2_controller.go | 37 +- .../pkg/runtime/v1alpha2/v1alpha2_state.go | 1 + internal/integration/api/node-labels.go | 184 ++++++++++ pkg/kubernetes/kubernetes.go | 8 +- .../api/resource/definitions/k8s/k8s.pb.go | 335 +++++++++++------- .../definitions/k8s/k8s_vtproto.pb.go | 182 ++++++++++ pkg/machinery/config/provider.go | 4 + .../types/v1alpha1/v1alpha1_provider.go | 5 + .../config/types/v1alpha1/v1alpha1_types.go | 6 + .../types/v1alpha1/v1alpha1_types_doc.go | 9 +- .../types/v1alpha1/v1alpha1_validation.go | 5 + .../v1alpha1/v1alpha1_validation_test.go | 26 ++ .../types/v1alpha1/zz_generated.deepcopy.go | 7 + pkg/machinery/constants/constants.go | 3 + pkg/machinery/labels/validate.go | 132 +++++++ pkg/machinery/labels/validate_test.go | 57 +++ .../resources/k8s/admissioncontrol_config.go | 3 - .../resources/k8s/deep_copy.generated.go | 30 +- pkg/machinery/resources/k8s/k8s.go | 3 + .../resources/k8s/node_label_spec.go | 75 ++++ website/content/v1.3/reference/api.md | 17 + .../content/v1.3/reference/configuration.md | 4 + .../editing-machine-configuration.md | 1 + 36 files changed, 1815 insertions(+), 201 deletions(-) rename internal/app/machined/pkg/controllers/{config/k8s_address_filter.go => k8s/address_filter.go} (88%) rename internal/app/machined/pkg/controllers/{config/k8s_address_filter_test.go => k8s/address_filter_test.go} (95%) rename internal/app/machined/pkg/controllers/{config/k8s_control_plane.go => k8s/control_plane.go} (87%) rename internal/app/machined/pkg/controllers/{config/k8s_control_plane_test.go => k8s/control_plane_test.go} (97%) create mode 100644 internal/app/machined/pkg/controllers/k8s/node_label_spec.go create mode 100644 internal/app/machined/pkg/controllers/k8s/node_label_spec_test.go create mode 100644 internal/app/machined/pkg/controllers/k8s/node_labels_apply.go create mode 100644 internal/app/machined/pkg/controllers/k8s/node_labels_apply_test.go create mode 100644 internal/integration/api/node-labels.go create mode 100644 pkg/machinery/labels/validate.go create mode 100644 pkg/machinery/labels/validate_test.go create mode 100644 pkg/machinery/resources/k8s/node_label_spec.go diff --git a/api/resource/definitions/k8s/k8s.proto b/api/resource/definitions/k8s/k8s.proto index c2e998afe..0a71e629e 100755 --- a/api/resource/definitions/k8s/k8s.proto +++ b/api/resource/definitions/k8s/k8s.proto @@ -148,6 +148,12 @@ message NodeIPSpec { repeated common.NetIP addresses = 1; } +// NodeLabelSpecSpec represents a label that's attached to a Talos node. +message NodeLabelSpecSpec { + string key = 1; + string value = 2; +} + // NodenameSpec describes Kubernetes nodename. message NodenameSpec { string nodename = 1; diff --git a/go.mod b/go.mod index 934fb778d..0d55097a3 100644 --- a/go.mod +++ b/go.mod @@ -153,6 +153,7 @@ require ( github.com/ProtonMail/gopenpgp/v2 v2.4.10 // indirect github.com/adrg/xdg v0.4.0 // indirect github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index 58f94b637..28050a881 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,7 @@ github.com/aws/aws-sdk-go v1.44.136/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw= github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/hack/release.toml b/hack/release.toml index 1b8d2331a..a646b22fc 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -208,7 +208,23 @@ system ``` > Note: `cgroupsv1` is deprecated and it should be used only for compatibility with workloads which don't support `cgroupsv2` yet. +""" + [notes.nodelabels] + title = "Node Labels" + description = """\ +Talos now supports specifying node labels in the machine configuration: + +```yaml +machine: + nodeLabels: + rack: rack1a + zone: us-east-1a +``` + +Changes to the node labels will be applied immediately without `kubelet` restart. + +Talos keeps track of the owned node labels in the `talos.dev/owned-labels` annotation. """ [make_deps] diff --git a/internal/app/machined/pkg/controllers/config/k8s_address_filter.go b/internal/app/machined/pkg/controllers/k8s/address_filter.go similarity index 88% rename from internal/app/machined/pkg/controllers/config/k8s_address_filter.go rename to internal/app/machined/pkg/controllers/k8s/address_filter.go index 582685140..8eb070fdd 100644 --- a/internal/app/machined/pkg/controllers/config/k8s_address_filter.go +++ b/internal/app/machined/pkg/controllers/k8s/address_filter.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package config +package k8s import ( "context" @@ -20,16 +20,16 @@ import ( "github.com/siderolabs/talos/pkg/machinery/resources/network" ) -// K8sAddressFilterController creates NodeAddressFilters based on machine configuration. -type K8sAddressFilterController struct{} +// AddressFilterController creates NodeAddressFilters based on machine configuration. +type AddressFilterController struct{} // Name implements controller.Controller interface. -func (ctrl *K8sAddressFilterController) Name() string { - return "network.K8sAddressFilterController" +func (ctrl *AddressFilterController) Name() string { + return "k8s.AddressFilterController" } // Inputs implements controller.Controller interface. -func (ctrl *K8sAddressFilterController) Inputs() []controller.Input { +func (ctrl *AddressFilterController) Inputs() []controller.Input { return []controller.Input{ { Namespace: config.NamespaceName, @@ -41,7 +41,7 @@ func (ctrl *K8sAddressFilterController) Inputs() []controller.Input { } // Outputs implements controller.Controller interface. -func (ctrl *K8sAddressFilterController) Outputs() []controller.Output { +func (ctrl *AddressFilterController) Outputs() []controller.Output { return []controller.Output{ { Type: network.NodeAddressFilterType, @@ -53,7 +53,7 @@ func (ctrl *K8sAddressFilterController) Outputs() []controller.Output { // Run implements controller.Controller interface. // //nolint:gocyclo -func (ctrl *K8sAddressFilterController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { +func (ctrl *AddressFilterController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { for { select { case <-ctx.Done(): diff --git a/internal/app/machined/pkg/controllers/config/k8s_address_filter_test.go b/internal/app/machined/pkg/controllers/k8s/address_filter_test.go similarity index 95% rename from internal/app/machined/pkg/controllers/config/k8s_address_filter_test.go rename to internal/app/machined/pkg/controllers/k8s/address_filter_test.go index 6ea58a740..057e51397 100644 --- a/internal/app/machined/pkg/controllers/config/k8s_address_filter_test.go +++ b/internal/app/machined/pkg/controllers/k8s/address_filter_test.go @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. //nolint:dupl -package config_test +package k8s_test import ( "context" @@ -22,7 +22,7 @@ import ( "github.com/siderolabs/go-retry/retry" "github.com/stretchr/testify/suite" - configctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/config" + k8sctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/k8s" "github.com/siderolabs/talos/pkg/logging" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/resources/config" @@ -53,7 +53,7 @@ func (suite *K8sAddressFilterSuite) SetupTest() { suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) suite.Require().NoError(err) - suite.Require().NoError(suite.runtime.RegisterController(&configctrl.K8sAddressFilterController{})) + suite.Require().NoError(suite.runtime.RegisterController(&k8sctrl.AddressFilterController{})) suite.startRuntime() } diff --git a/internal/app/machined/pkg/controllers/config/k8s_control_plane.go b/internal/app/machined/pkg/controllers/k8s/control_plane.go similarity index 87% rename from internal/app/machined/pkg/controllers/config/k8s_control_plane.go rename to internal/app/machined/pkg/controllers/k8s/control_plane.go index 6b53b120b..7994d8c9e 100644 --- a/internal/app/machined/pkg/controllers/config/k8s_control_plane.go +++ b/internal/app/machined/pkg/controllers/k8s/control_plane.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package config +package k8s import ( "context" @@ -26,16 +26,16 @@ import ( "github.com/siderolabs/talos/pkg/machinery/resources/k8s" ) -// K8sControlPlaneController manages Kubernetes control plane resources based on configuration. -type K8sControlPlaneController struct{} +// ControlPlaneController manages Kubernetes control plane resources based on configuration. +type ControlPlaneController struct{} // Name implements controller.Controller interface. -func (ctrl *K8sControlPlaneController) Name() string { - return "config.K8sControlPlaneController" +func (ctrl *ControlPlaneController) Name() string { + return "k8s.ControlPlaneController" } // Inputs implements controller.Controller interface. -func (ctrl *K8sControlPlaneController) Inputs() []controller.Input { +func (ctrl *ControlPlaneController) Inputs() []controller.Input { return []controller.Input{ { Namespace: config.NamespaceName, @@ -53,7 +53,7 @@ func (ctrl *K8sControlPlaneController) Inputs() []controller.Input { } // Outputs implements controller.Controller interface. -func (ctrl *K8sControlPlaneController) Outputs() []controller.Output { +func (ctrl *ControlPlaneController) Outputs() []controller.Output { return []controller.Output{ { Type: k8s.AdmissionControlConfigType, @@ -89,7 +89,7 @@ func (ctrl *K8sControlPlaneController) Outputs() []controller.Output { // Run implements controller.Controller interface. // //nolint:gocyclo -func (ctrl *K8sControlPlaneController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { +func (ctrl *ControlPlaneController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { for { select { case <-ctx.Done(): @@ -158,10 +158,10 @@ func convertVolumes(volumes []talosconfig.VolumeMount) []k8s.ExtraVolume { }) } -func (ctrl *K8sControlPlaneController) manageAPIServerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageAPIServerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { var cloudProvider string if cfgProvider.Cluster().ExternalCloudProvider().Enabled() { - cloudProvider = "external" + cloudProvider = "external" //nolint:goconst } advertisedAddress := "$(POD_IP)" @@ -188,7 +188,7 @@ func (ctrl *K8sControlPlaneController) manageAPIServerConfig(ctx context.Context }) } -func (ctrl *K8sControlPlaneController) manageAdmissionControlConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageAdmissionControlConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { spec := k8s.AdmissionControlConfigSpec{} for _, cfg := range cfgProvider.Cluster().APIServer().AdmissionControl() { @@ -207,7 +207,7 @@ func (ctrl *K8sControlPlaneController) manageAdmissionControlConfig(ctx context. }) } -func (ctrl *K8sControlPlaneController) manageAuditPolicyConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageAuditPolicyConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { spec := k8s.AuditPolicyConfigSpec{} spec.Config = cfgProvider.Cluster().APIServer().AuditPolicy() @@ -219,7 +219,7 @@ func (ctrl *K8sControlPlaneController) manageAuditPolicyConfig(ctx context.Conte }) } -func (ctrl *K8sControlPlaneController) manageControllerManagerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageControllerManagerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { var cloudProvider string if cfgProvider.Cluster().ExternalCloudProvider().Enabled() { cloudProvider = "external" @@ -241,7 +241,7 @@ func (ctrl *K8sControlPlaneController) manageControllerManagerConfig(ctx context }) } -func (ctrl *K8sControlPlaneController) manageSchedulerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageSchedulerConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { return r.Modify(ctx, k8s.NewSchedulerConfig(), func(r resource.Resource) error { *r.(*k8s.SchedulerConfig).TypedSpec() = k8s.SchedulerConfigSpec{ Enabled: !cfgProvider.Machine().Controlplane().Scheduler().Disabled(), @@ -255,7 +255,7 @@ func (ctrl *K8sControlPlaneController) manageSchedulerConfig(ctx context.Context }) } -func (ctrl *K8sControlPlaneController) manageManifestsConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageManifestsConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { dnsServiceIPs, err := cfgProvider.Cluster().Network().DNSServiceIPs() if err != nil { return fmt.Errorf("error calculating DNS service IPs: %w", err) @@ -311,7 +311,7 @@ func (ctrl *K8sControlPlaneController) manageManifestsConfig(ctx context.Context }) } -func (ctrl *K8sControlPlaneController) manageExtraManifestsConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { +func (ctrl *ControlPlaneController) manageExtraManifestsConfig(ctx context.Context, r controller.Runtime, logger *zap.Logger, cfgProvider talosconfig.Provider) error { return r.Modify(ctx, k8s.NewExtraManifestsConfig(), func(r resource.Resource) error { spec := k8s.ExtraManifestsConfigSpec{} @@ -354,7 +354,7 @@ func (ctrl *K8sControlPlaneController) manageExtraManifestsConfig(ctx context.Co }) } -func (ctrl *K8sControlPlaneController) teardownAll(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { +func (ctrl *ControlPlaneController) teardownAll(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { return nil } diff --git a/internal/app/machined/pkg/controllers/config/k8s_control_plane_test.go b/internal/app/machined/pkg/controllers/k8s/control_plane_test.go similarity index 97% rename from internal/app/machined/pkg/controllers/config/k8s_control_plane_test.go rename to internal/app/machined/pkg/controllers/k8s/control_plane_test.go index 59404fab6..e8e736275 100644 --- a/internal/app/machined/pkg/controllers/config/k8s_control_plane_test.go +++ b/internal/app/machined/pkg/controllers/k8s/control_plane_test.go @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. //nolint:dupl -package config_test +package k8s_test import ( "context" @@ -25,7 +25,7 @@ import ( "github.com/siderolabs/go-retry/retry" "github.com/stretchr/testify/suite" - configctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/config" + k8sctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/k8s" "github.com/siderolabs/talos/pkg/logging" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/machine" @@ -56,7 +56,7 @@ func (suite *K8sControlPlaneSuite) SetupTest() { suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) suite.Require().NoError(err) - suite.Require().NoError(suite.runtime.RegisterController(&configctrl.K8sControlPlaneController{})) + suite.Require().NoError(suite.runtime.RegisterController(&k8sctrl.ControlPlaneController{})) suite.startRuntime() } @@ -417,7 +417,7 @@ func (suite *K8sControlPlaneSuite) TearDownTest() { suite.state.Destroy( context.Background(), k8s.NewAPIServerConfig().Metadata(), - state.WithDestroyOwner("config.K8sControlPlaneController"), + state.WithDestroyOwner("k8s.ControlPlaneController"), ), ) } diff --git a/internal/app/machined/pkg/controllers/k8s/node_label_spec.go b/internal/app/machined/pkg/controllers/k8s/node_label_spec.go new file mode 100644 index 000000000..c6c562bd2 --- /dev/null +++ b/internal/app/machined/pkg/controllers/k8s/node_label_spec.go @@ -0,0 +1,103 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package k8s + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/go-pointer" + "go.uber.org/zap" + + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/k8s" +) + +// NodeLabelSpecController manages k8s.NodeLabelsConfig based on configuration. +type NodeLabelSpecController struct{} + +// Name implements controller.Controller interface. +func (ctrl *NodeLabelSpecController) Name() string { + return "k8s.NodeLabelSpecController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *NodeLabelSpecController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: pointer.To(config.V1Alpha1ID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *NodeLabelSpecController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: k8s.NodeLabelSpecType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *NodeLabelSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + var nodeLabels map[string]string + + cfg, err := safe.ReaderGet[*config.MachineConfig](ctx, r, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined)) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error getting config: %w", err) + } + } else { + nodeLabels = cfg.Config().Machine().NodeLabels() + } + + for key, value := range nodeLabels { + if err = r.Modify(ctx, k8s.NewNodeLabelSpec(key), func(r resource.Resource) error { + r.(*k8s.NodeLabelSpec).TypedSpec().Key = key + r.(*k8s.NodeLabelSpec).TypedSpec().Value = value + + return nil + }); err != nil { + return fmt.Errorf("error updating node label spec: %w", err) + } + } + + labelSpecs, err := safe.ReaderList[*k8s.NodeLabelSpec](ctx, r, k8s.NewNodeLabelSpec("").Metadata()) + if err != nil { + return fmt.Errorf("error getting node label specs: %w", err) + } + + for iter := safe.IteratorFromList(labelSpecs); iter.Next(); { + labelSpec := iter.Value() + + _, touched := nodeLabels[labelSpec.TypedSpec().Key] + if touched { + continue + } + + if err = r.Destroy(ctx, labelSpec.Metadata()); err != nil { + return fmt.Errorf("error destroying node label spec: %w", err) + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/k8s/node_label_spec_test.go b/internal/app/machined/pkg/controllers/k8s/node_label_spec_test.go new file mode 100644 index 000000000..470d375e7 --- /dev/null +++ b/internal/app/machined/pkg/controllers/k8s/node_label_spec_test.go @@ -0,0 +1,274 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//nolint:dupl +package k8s_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/controller/runtime" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/cosi-project/runtime/pkg/state/impl/inmem" + "github.com/cosi-project/runtime/pkg/state/impl/namespaced" + "github.com/siderolabs/go-retry/retry" + "github.com/stretchr/testify/suite" + + k8sctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/k8s" + "github.com/siderolabs/talos/pkg/logging" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/machine" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/k8s" +) + +type NodeLabelsSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + //nolint:containedctx + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *NodeLabelsSuite) createAndStartRuntime() { + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute) + + suite.state = state.WrapCore(namespaced.NewState(inmem.Build)) + + var err error + + suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) + suite.Require().NoError(err) + + suite.Require().NoError(suite.runtime.RegisterController(&k8sctrl.NodeLabelSpecController{})) + + suite.startRuntime() + + suite.setupMachineType() + + suite.createNodename() +} + +func (suite *NodeLabelsSuite) SetupTest() { + suite.createAndStartRuntime() +} + +func (suite *NodeLabelsSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *NodeLabelsSuite) assertResource( + md resource.Metadata, + check func(res resource.Resource) error, +) func() error { + return func() error { + r, err := suite.state.Get(suite.ctx, md) + if err != nil { + if state.IsNotFoundError(err) { + return retry.ExpectedError(err) + } + + return err + } + + return check(r) + } +} + +func (suite *NodeLabelsSuite) setupMachineType() { + machineType := config.NewMachineType() + machineType.SetMachineType(machine.TypeControlPlane) + + suite.Require().NoError(suite.state.Create(suite.ctx, machineType)) +} + +func mcWithNodeLabels(labels map[string]string) *config.MachineConfig { + return config.NewMachineConfig( + &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineNodeLabels: labels, + }, + }) +} + +func (suite *NodeLabelsSuite) createNodeLabelsConfig(labels map[string]string) { + mc := mcWithNodeLabels(labels) + + suite.Require().NoError(suite.state.Create(suite.ctx, mc)) +} + +func (suite *NodeLabelsSuite) createNodename() { + nodeName := k8s.NewNodename(k8s.NamespaceName, k8s.NodenameID) + suite.Require().NoError(suite.state.Create(suite.ctx, nodeName)) +} + +func (suite *NodeLabelsSuite) changeNodeLabelsConfig(labels map[string]string) { + mc := mcWithNodeLabels(labels) + + oldCfg, err := suite.state.Get(suite.ctx, mc.Metadata()) + if err != nil { + if state.IsNotFoundError(err) { + suite.Require().NoError( + suite.state.Create(suite.ctx, mc), + ) + + return + } + + suite.Require().NoError(err) + } + + mc.Metadata().SetVersion(oldCfg.Metadata().Version()) + + suite.Require().NoError( + suite.state.Update(suite.ctx, mc), + ) +} + +func (suite *NodeLabelsSuite) assertInexistentLabel(expectedLabel string) { + suite.Assert().NoError( + retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + md := resource.NewMetadata( + k8s.NamespaceName, + k8s.NodeLabelSpecType, + expectedLabel, + resource.VersionUndefined, + ) + + _, err := suite.state.Get(suite.ctx, md) + if err == nil { + return retry.ExpectedError(fmt.Errorf("resource should be destroyed: %v", md)) + } + + if !state.IsNotFoundError(err) { + return err + } + + return nil + }, + ), + ) +} + +func (suite *NodeLabelsSuite) assertLabel(expectedLabel, oldValue, expectedValue string) { + suite.Assert().NoError( + retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + if err := suite.assertResource( + resource.NewMetadata( + k8s.NamespaceName, + k8s.NodeLabelSpecType, + expectedLabel, + resource.VersionUndefined, + ), + func(res resource.Resource) error { + spec := res.(*k8s.NodeLabelSpec).TypedSpec() + + suite.Assert().Equal( + expectedLabel, + spec.Key, + ) + + if oldValue != "" && spec.Value == oldValue { + return retry.ExpectedError(fmt.Errorf("old value still set: %q", oldValue)) + } + + suite.Assert().Equal( + expectedValue, + spec.Value, + ) + + return nil + }, + )(); err != nil { + return err + } + + return nil + }, + ), + ) +} + +func (suite *NodeLabelsSuite) TestAddLabel() { + // given + expectedLabel := "expectedLabel" + expectedValue := "expectedValue" + + // when + suite.createNodeLabelsConfig(map[string]string{ + expectedLabel: expectedValue, + }) + + // then + suite.assertLabel(expectedLabel, "", expectedValue) +} + +func (suite *NodeLabelsSuite) TestChangeLabel() { + // given + expectedLabel := "someLabel" + oldValue := "oldValue" + expectedValue := "newValue" + + // when + suite.createNodeLabelsConfig(map[string]string{ + expectedLabel: oldValue, + }) + + suite.assertLabel(expectedLabel, "", oldValue) + + suite.changeNodeLabelsConfig(map[string]string{ + expectedLabel: expectedValue, + }) + + // then + suite.assertLabel(expectedLabel, oldValue, expectedValue) +} + +func (suite *NodeLabelsSuite) TestDeleteLabel() { + // given + expectedLabel := "label" + expectedValue := "labelValue" + + // when + suite.createNodeLabelsConfig(map[string]string{ + expectedLabel: expectedValue, + }) + suite.assertLabel(expectedLabel, "", expectedValue) + + suite.changeNodeLabelsConfig(map[string]string{}) + + // then + suite.assertInexistentLabel(expectedLabel) +} + +func (suite *NodeLabelsSuite) TearDownTest() { + suite.T().Log("tear down") + + suite.ctxCancel() + + suite.wg.Wait() +} + +func TestNodeLabelsSuite(t *testing.T) { + suite.Run(t, new(NodeLabelsSuite)) +} diff --git a/internal/app/machined/pkg/controllers/k8s/node_labels_apply.go b/internal/app/machined/pkg/controllers/k8s/node_labels_apply.go new file mode 100644 index 000000000..1b6c171ff --- /dev/null +++ b/internal/app/machined/pkg/controllers/k8s/node_labels_apply.go @@ -0,0 +1,280 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package k8s + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/gen/maps" + "github.com/siderolabs/gen/slices" + "github.com/siderolabs/go-pointer" + "github.com/siderolabs/go-retry/retry" + "go.uber.org/zap" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/siderolabs/talos/pkg/conditions" + "github.com/siderolabs/talos/pkg/kubernetes" + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/k8s" + "github.com/siderolabs/talos/pkg/machinery/resources/secrets" +) + +// NodeLabelsApplyController watches k8s.NodeLabelSpec's and applies them to the k8s Node object. +type NodeLabelsApplyController struct{} + +// Name implements controller.Controller interface. +func (ctrl *NodeLabelsApplyController) Name() string { + return "k8s.NodeLabelsApplyController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *NodeLabelsApplyController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: k8s.NamespaceName, + Type: k8s.NodeLabelSpecType, + Kind: controller.InputWeak, + }, + { + Namespace: secrets.NamespaceName, + Type: secrets.KubernetesRootType, + ID: pointer.To(secrets.KubernetesRootID), + Kind: controller.InputWeak, + }, + { + Namespace: k8s.NamespaceName, + Type: k8s.NodenameType, + ID: pointer.To(k8s.NodenameID), + Kind: controller.InputWeak, + }, + { + Namespace: config.NamespaceName, + Type: config.MachineTypeType, + ID: pointer.To(config.MachineTypeID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *NodeLabelsApplyController) Outputs() []controller.Output { + return nil +} + +// Run implements controller.Controller interface. +func (ctrl *NodeLabelsApplyController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + if err := ctrl.reconcileWithK8s(ctx, r, logger); err != nil { + return err + } + } +} + +func (ctrl *NodeLabelsApplyController) getNodeLabelSpecs(ctx context.Context, r controller.Runtime) (map[string]string, error) { + items, err := safe.ReaderList[*k8s.NodeLabelSpec](ctx, r, resource.NewMetadata(k8s.NamespaceName, k8s.NodeLabelSpecType, "", resource.VersionUndefined)) + if err != nil { + return nil, fmt.Errorf("error listing node label spec resources: %w", err) + } + + result := make(map[string]string, items.Len()) + + for iter := safe.IteratorFromList(items); iter.Next(); { + result[iter.Value().TypedSpec().Key] = iter.Value().TypedSpec().Value + } + + return result, nil +} + +func (ctrl *NodeLabelsApplyController) getK8sClient(ctx context.Context, r controller.Runtime, logger *zap.Logger) (*kubernetes.Client, error) { + machineType, err := safe.ReaderGet[*config.MachineType](ctx, r, resource.NewMetadata(config.NamespaceName, config.MachineTypeType, config.MachineTypeID, resource.VersionUndefined)) + if err != nil { + return nil, fmt.Errorf("error getting machine type: %w", err) + } + + if machineType.MachineType().IsControlPlane() { + k8sRoot, err := safe.ReaderGet[*secrets.KubernetesRoot](ctx, r, resource.NewMetadata(secrets.NamespaceName, secrets.KubernetesRootType, secrets.KubernetesRootID, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + return nil, nil + } + + return nil, fmt.Errorf("failed to get kubernetes config: %w", err) + } + + k8sRootSpec := k8sRoot.TypedSpec() + + return kubernetes.NewTemporaryClientFromPKI(k8sRootSpec.CA, k8sRootSpec.Endpoint) + } + + logger.Debug("waiting for kubelet client config", zap.String("file", constants.KubeletKubeconfig)) + + if err := conditions.WaitForKubeconfigReady(constants.KubeletKubeconfig).Wait(ctx); err != nil { + return nil, err + } + + return kubernetes.NewClientFromKubeletKubeconfig() +} + +func (ctrl *NodeLabelsApplyController) reconcileWithK8s( + ctx context.Context, + r controller.Runtime, + logger *zap.Logger, +) error { + nodenameResource, err := safe.ReaderGet[*k8s.Nodename](ctx, r, resource.NewMetadata(k8s.NamespaceName, k8s.NodenameType, k8s.NodenameID, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + return nil + } + + return err + } + + nodename := nodenameResource.TypedSpec().Nodename + + k8sClient, err := ctrl.getK8sClient(ctx, r, logger) + if err != nil { + return fmt.Errorf("error building kubernetes client: %w", err) + } + + if k8sClient == nil { + // not ready yet + return nil + } + + defer k8sClient.Close() //nolint:errcheck + + nodeLabelSpecs, err := ctrl.getNodeLabelSpecs(ctx, r) + if err != nil { + return err + } + + return ctrl.syncLabels(ctx, logger, k8sClient, nodename, nodeLabelSpecs) +} + +func (ctrl *NodeLabelsApplyController) syncLabels( + ctx context.Context, + logger *zap.Logger, + k8sClient *kubernetes.Client, + nodeName string, + nodeLabelSpecs map[string]string, +) error { + // run several attempts retrying conflict errors + return retry.Constant(10*time.Second, retry.WithUnits(100*time.Millisecond)).RetryWithContext(ctx, func(ctx context.Context) error { + err := ctrl.syncLabelsOnce(ctx, logger, k8sClient, nodeName, nodeLabelSpecs) + + if err != nil && apierrors.IsConflict(err) { + return retry.ExpectedError(err) + } + + return err + }) +} + +func (ctrl *NodeLabelsApplyController) syncLabelsOnce( + ctx context.Context, + logger *zap.Logger, + k8sClient *kubernetes.Client, + nodeName string, + nodeLabelSpecs map[string]string, +) error { + node, err := k8sClient.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error getting node: %w", err) + } + + if node.Labels == nil { + node.Labels = make(map[string]string) + } + + ownedJSON := []byte(node.Annotations[constants.AnnotationOwnedLabels]) + ownedLabels := []string{} + + if len(ownedJSON) > 0 { + if err = json.Unmarshal(ownedJSON, &ownedLabels); err != nil { + return fmt.Errorf("error unmarshaling owned labels: %w", err) + } + } + + ownedLabelsMap := slices.ToSet(ownedLabels) + if ownedLabelsMap == nil { + ownedLabelsMap = map[string]struct{}{} + } + + ctrl.ApplyLabels(logger, node, ownedLabelsMap, nodeLabelSpecs) + + ownedLabels = maps.Keys(ownedLabelsMap) + sort.Strings(ownedLabels) + + if len(ownedLabels) > 0 { + ownedJSON, err = json.Marshal(ownedLabels) + if err != nil { + return fmt.Errorf("error marshaling owned labels: %w", err) + } + + node.Annotations[constants.AnnotationOwnedLabels] = string(ownedJSON) + } else { + delete(node.Annotations, constants.AnnotationOwnedLabels) + } + + _, err = k8sClient.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{}) + + return err +} + +// ApplyLabels performs the inner loop of the node label reconciliation. +// +// This method is exported for testing purposes. +func (ctrl *NodeLabelsApplyController) ApplyLabels(logger *zap.Logger, node *v1.Node, ownedLabels map[string]struct{}, nodeLabelSpecs map[string]string) { + // set labels from the spec + for key, value := range nodeLabelSpecs { + currentValue, exists := node.Labels[key] + + // label is not set on the node yet, so take it over + if !exists { + node.Labels[key] = value + ownedLabels[key] = struct{}{} + + continue + } + + // no change to the label, skip it + if currentValue == value { + continue + } + + if _, owned := ownedLabels[key]; !owned { + logger.Debug("skipping label update, label is not owned", zap.String("key", key), zap.String("value", value)) + + continue + } + + node.Labels[key] = value + } + + // remove labels which are owned but are not in the spec + for key := range ownedLabels { + if _, exists := nodeLabelSpecs[key]; !exists { + delete(node.Labels, key) + delete(ownedLabels, key) + } + } +} diff --git a/internal/app/machined/pkg/controllers/k8s/node_labels_apply_test.go b/internal/app/machined/pkg/controllers/k8s/node_labels_apply_test.go new file mode 100644 index 000000000..de3077d11 --- /dev/null +++ b/internal/app/machined/pkg/controllers/k8s/node_labels_apply_test.go @@ -0,0 +1,135 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package k8s_test + +import ( + "sort" + "testing" + + "github.com/siderolabs/gen/maps" + "github.com/siderolabs/gen/slices" + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zaptest" + v1 "k8s.io/api/core/v1" + + "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/k8s" +) + +func TestApplyLabels(t *testing.T) { + ctrl := &k8s.NodeLabelsApplyController{} + logger := zaptest.NewLogger(t) + + for _, tt := range []struct { + name string + inputLabels map[string]string + ownedLabels []string + labelSpec map[string]string + + expectedLabels map[string]string + expectedOwnedLabels []string + }{ + { + name: "empty", + inputLabels: map[string]string{}, + ownedLabels: []string{}, + labelSpec: map[string]string{}, + + expectedLabels: map[string]string{}, + expectedOwnedLabels: []string{}, + }, + { + name: "initial set labels", + inputLabels: map[string]string{ + "hostname": "foo", + }, + ownedLabels: []string{}, + labelSpec: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + + expectedLabels: map[string]string{ + "hostname": "foo", + "label1": "value1", + "label2": "value2", + }, + expectedOwnedLabels: []string{ + "label1", + "label2", + }, + }, + { + name: "update owned labels", + inputLabels: map[string]string{ + "hostname": "foo", + "label1": "value1", + "label2": "value2", + }, + ownedLabels: []string{ + "label1", + "label2", + }, + labelSpec: map[string]string{ + "label1": "value3", + }, + + expectedLabels: map[string]string{ + "hostname": "foo", + "label1": "value3", + }, + expectedOwnedLabels: []string{ + "label1", + }, + }, + { + name: "ignore not owned labels", + inputLabels: map[string]string{ + "hostname": "foo", + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + ownedLabels: []string{ + "label2", + }, + labelSpec: map[string]string{ + "label1": "value3", + "label2": "value2", + }, + + expectedLabels: map[string]string{ + "hostname": "foo", + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + expectedOwnedLabels: []string{ + "label2", + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + node := &v1.Node{} + node.Labels = tt.inputLabels + + ownedLabels := slices.ToSet(tt.ownedLabels) + if ownedLabels == nil { + ownedLabels = map[string]struct{}{} + } + + ctrl.ApplyLabels(logger, node, ownedLabels, tt.labelSpec) + + newOwnedLabels := maps.Keys(ownedLabels) + if newOwnedLabels == nil { + newOwnedLabels = []string{} + } + + sort.Strings(newOwnedLabels) + + assert.Equal(t, tt.expectedLabels, node.Labels) + assert.Equal(t, tt.expectedOwnedLabels, newOwnedLabels) + }) + } +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go index 326842545..e950aa232 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go @@ -133,6 +133,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error { // * .machine.registries (note that auth is not applied immediately, containerd limitation) // * .machine.pods // * .machine.seccompProfiles + // * .machine.nodeLabels // * .machine.features.kubernetesTalosAPIAccess newConfig.ConfigDebug = currentConfig.ConfigDebug newConfig.ClusterConfig = currentConfig.ClusterConfig @@ -151,6 +152,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error { newConfig.MachineConfig.MachineRegistries = currentConfig.MachineConfig.MachineRegistries newConfig.MachineConfig.MachinePods = currentConfig.MachineConfig.MachinePods newConfig.MachineConfig.MachineSeccompProfiles = currentConfig.MachineConfig.MachineSeccompProfiles + newConfig.MachineConfig.MachineNodeLabels = currentConfig.MachineConfig.MachineNodeLabels if newConfig.MachineConfig.MachineFeatures != nil && currentConfig.MachineConfig.MachineFeatures != nil { newConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = currentConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index e9dcfe492..85e914a5b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -85,27 +85,18 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error go ctrl.watchMachineConfig(ctx) for _, c := range []controller.Controller{ - &v1alpha1.ServiceController{ - // V1Events - V1Alpha1Events: ctrl.v1alpha1Runtime.Events(), - }, - &timecontrollers.SyncController{ - V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), - }, &cluster.AffiliateMergeController{}, &cluster.ConfigController{}, &cluster.DiscoveryServiceController{}, &cluster.EndpointController{}, - &cluster.LocalAffiliateController{}, - &cluster.MemberController{}, &cluster.KubernetesPullController{}, &cluster.KubernetesPushController{}, + &cluster.LocalAffiliateController{}, + &cluster.MemberController{}, &cluster.NodeIdentityController{ V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, &config.MachineTypeController{}, - &config.K8sAddressFilterController{}, - &config.K8sControlPlaneController{}, &cri.SeccompProfileController{}, &cri.SeccompProfileFileController{ V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), @@ -125,6 +116,8 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error &hardware.SystemInfoController{ V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, + &k8s.AddressFilterController{}, + &k8s.ControlPlaneController{}, &k8s.ControlPlaneStaticPodController{}, &k8s.EndpointController{}, &k8s.ExtraManifestController{}, @@ -137,18 +130,20 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, &k8s.KubeletStaticPodController{}, - &k8s.ManifestController{}, &k8s.ManifestApplyController{}, - &k8s.NodeIPController{}, + &k8s.ManifestController{}, &k8s.NodeIPConfigController{}, + &k8s.NodeIPController{}, + &k8s.NodeLabelSpecController{}, + &k8s.NodeLabelsApplyController{}, &k8s.NodenameController{}, &k8s.RenderConfigsStaticPodController{}, &k8s.RenderSecretsStaticPodController{}, &k8s.StaticPodConfigController{}, &k8s.StaticPodServerController{}, &kubeaccess.ConfigController{}, - &kubeaccess.EndpointController{}, &kubeaccess.CRDController{}, + &kubeaccess.EndpointController{}, &kubespan.ConfigController{}, &kubespan.EndpointController{}, &kubespan.IdentityController{}, @@ -178,8 +173,8 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error Cmdline: procfs.ProcCmdline(), }, &network.LinkMergeController{}, - &network.LinkStatusController{}, &network.LinkSpecController{}, + &network.LinkStatusController{}, &network.NodeAddressController{}, &network.OperatorConfigController{ Cmdline: procfs.ProcCmdline(), @@ -205,8 +200,8 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error Cmdline: procfs.ProcCmdline(), }, &network.RouteMergeController{}, - &network.RouteStatusController{}, &network.RouteSpecController{}, + &network.RouteStatusController{}, &network.StatusController{}, &network.TimeServerConfigController{ Cmdline: procfs.ProcCmdline(), @@ -243,17 +238,23 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error &runtimecontrollers.MachineStatusPublisherController{ V1Alpha1Events: ctrl.v1alpha1Runtime.Events(), }, - &secrets.APIController{}, &secrets.APICertSANsController{}, + &secrets.APIController{}, &secrets.EtcdController{}, &secrets.KubeletController{}, - &secrets.KubernetesController{}, &secrets.KubernetesCertSANsController{}, + &secrets.KubernetesController{}, &secrets.RootController{}, &secrets.TrustdController{}, &siderolink.ManagerController{ Cmdline: procfs.ProcCmdline(), }, + &timecontrollers.SyncController{ + V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), + }, + &v1alpha1.ServiceController{ + V1Alpha1Events: ctrl.v1alpha1Runtime.Events(), + }, } { if err := ctrl.controllerRuntime.RegisterController(c); err != nil { return err diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index e5ed172b2..92f2d75bb 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -92,6 +92,7 @@ func NewState() (*State, error) { &cluster.Member{}, &config.MachineConfig{}, &config.MachineType{}, + &k8s.NodeLabelSpec{}, &cri.SeccompProfile{}, &etcd.Config{}, &etcd.PKIStatus{}, diff --git a/internal/integration/api/node-labels.go b/internal/integration/api/node-labels.go new file mode 100644 index 000000000..73294ea06 --- /dev/null +++ b/internal/integration/api/node-labels.go @@ -0,0 +1,184 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build integration_api + +package api + +import ( + "context" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "github.com/siderolabs/talos/internal/integration/base" + machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine" + "github.com/siderolabs/talos/pkg/machinery/client" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/machine" +) + +// NodeLabelsSuite verifies updating node labels via machine config. +type NodeLabelsSuite struct { + base.K8sSuite + + ctx context.Context //nolint:containedctx + ctxCancel context.CancelFunc +} + +// SuiteName ... +func (suite *NodeLabelsSuite) SuiteName() string { + return "api.NodeLabelsSuite" +} + +// SetupTest ... +func (suite *NodeLabelsSuite) SetupTest() { + // make sure API calls have timeout + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 5*time.Minute) +} + +// TearDownTest ... +func (suite *NodeLabelsSuite) TearDownTest() { + if suite.ctxCancel != nil { + suite.ctxCancel() + } +} + +// TestUpdateControlPlane verifies node label updates on control plane nodes. +func (suite *NodeLabelsSuite) TestUpdateControlPlane() { + node := suite.RandomDiscoveredNodeInternalIP(machine.TypeControlPlane) + + suite.testUpdate(node, true) +} + +// TestUpdateWorker verifies node label updates on worker nodes. +func (suite *NodeLabelsSuite) TestUpdateWorker() { + node := suite.RandomDiscoveredNodeInternalIP(machine.TypeWorker) + + suite.testUpdate(node, false) +} + +// testUpdate cycles through a set of node label updates reverting the change in the end. +func (suite *NodeLabelsSuite) testUpdate(node string, isControlplane bool) { + k8sNode, err := suite.GetK8sNodeByInternalIP(suite.ctx, node) + suite.Require().NoError(err) + + suite.T().Logf("updating labels on node %q (%q)", node, k8sNode.Name) + + watcher, err := suite.Clientset.CoreV1().Nodes().Watch(suite.ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=" + k8sNode.Name, + Watch: true, + }) + suite.Require().NoError(err) + + defer watcher.Stop() + + const stdLabelName = "kubernetes.io/hostname" + + stdLabelValue := k8sNode.Labels[stdLabelName] + + // set two new labels + suite.setNodeLabels(node, map[string]string{ + "talos.dev/test1": "value1", + "talos.dev/test2": "value2", + }) + + suite.waitUntil(watcher, map[string]string{ + "talos.dev/test1": "value1", + "talos.dev/test2": "value2", + }) + + // remove one label owned by Talos + suite.setNodeLabels(node, map[string]string{ + "talos.dev/test1": "foo", + }) + + suite.waitUntil(watcher, map[string]string{ + "talos.dev/test1": "foo", + "talos.dev/test2": "", + }) + + // on control plane node, try to override a label not owned by Talos + if isControlplane { + suite.setNodeLabels(node, map[string]string{ + "talos.dev/test1": "foo2", + stdLabelName: "bar", + }) + + suite.waitUntil(watcher, map[string]string{ + "talos.dev/test1": "foo2", + stdLabelName: stdLabelValue, + }) + } + + // remove all Talos Labels + suite.setNodeLabels(node, nil) + + suite.waitUntil(watcher, map[string]string{ + "talos.dev/test1": "", + "talos.dev/test2": "", + }) +} + +func (suite *NodeLabelsSuite) waitUntil(watcher watch.Interface, expectedLabels map[string]string) { +outer: + for { + select { + case ev := <-watcher.ResultChan(): + k8sNode, ok := ev.Object.(*v1.Node) + suite.Require().True(ok, "watch event is not of type v1.Node") + + suite.T().Logf("labels %v", k8sNode.Labels) + + for k, v := range expectedLabels { + if v == "" { + _, ok := k8sNode.Labels[k] + if ok { + suite.T().Logf("label %q is still present", k) + + continue outer + } + } + + if k8sNode.Labels[k] != v { + suite.T().Logf("label %q is not %q", k, v) + + continue outer + } + } + + return + case <-suite.ctx.Done(): + suite.T().Fatal("timeout") + } + } +} + +func (suite *NodeLabelsSuite) setNodeLabels(nodeIP string, nodeLabels map[string]string) { + nodeCtx := client.WithNode(suite.ctx, nodeIP) + + nodeConfig, err := suite.ReadConfigFromNode(nodeCtx) + suite.Require().NoError(err) + + nodeConfigRaw, ok := nodeConfig.Raw().(*v1alpha1.Config) + suite.Require().True(ok, "node config is not of type v1alpha1.Config") + + nodeConfigRaw.MachineConfig.MachineNodeLabels = nodeLabels + + bytes, err := nodeConfigRaw.Bytes() + suite.Require().NoError(err) + + _, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{ + Data: bytes, + Mode: machineapi.ApplyConfigurationRequest_NO_REBOOT, + }) + + suite.Require().NoError(err) +} + +func init() { + allSuites = append(allSuites, new(NodeLabelsSuite)) +} diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index fb3b38100..afa7675ea 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -224,9 +224,9 @@ func (h *Client) LabelNodeAsControlPlane(ctx context.Context, name string, taint } // The node may appear to have no labels at first, so we check for the - // existence of a well known label to ensure the patch will be successful. + // existence of a well known label to ensure that a patch will be successful. if _, found := n.ObjectMeta.Labels[corev1.LabelHostname]; !found { - return errors.New("could not find hostname label") + return fmt.Errorf("could not find hostname label") } oldData, err := json.Marshal(n) @@ -268,12 +268,12 @@ func (h *Client) LabelNodeAsControlPlane(ctx context.Context, name string, taint return fmt.Errorf("failed to create two way merge patch: %w", err) } - if _, err := h.CoreV1().Nodes().Patch(ctx, n.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { + if _, err := h.CoreV1().Nodes().Patch(ctx, name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { if apierrors.IsConflict(err) { return fmt.Errorf("unable to update node metadata due to conflict: %w", err) } - return fmt.Errorf("error patching node %q: %w", n.Name, err) + return fmt.Errorf("error patching node %q: %w", name, err) } return nil diff --git a/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go b/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go index 3f620feca..8bf7f4e17 100644 --- a/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go +++ b/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go @@ -1281,6 +1281,62 @@ func (x *NodeIPSpec) GetAddresses() []*common.NetIP { return nil } +// NodeLabelSpecSpec represents a label that's attached to a Talos node. +type NodeLabelSpecSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *NodeLabelSpecSpec) Reset() { + *x = NodeLabelSpecSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NodeLabelSpecSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeLabelSpecSpec) ProtoMessage() {} + +func (x *NodeLabelSpecSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeLabelSpecSpec.ProtoReflect.Descriptor instead. +func (*NodeLabelSpecSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{17} +} + +func (x *NodeLabelSpecSpec) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *NodeLabelSpecSpec) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + // NodenameSpec describes Kubernetes nodename. type NodenameSpec struct { state protoimpl.MessageState @@ -1294,7 +1350,7 @@ type NodenameSpec struct { func (x *NodenameSpec) Reset() { *x = NodenameSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[17] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1307,7 +1363,7 @@ func (x *NodenameSpec) String() string { func (*NodenameSpec) ProtoMessage() {} func (x *NodenameSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[17] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1320,7 +1376,7 @@ func (x *NodenameSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use NodenameSpec.ProtoReflect.Descriptor instead. func (*NodenameSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{17} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{18} } func (x *NodenameSpec) GetNodename() string { @@ -1353,7 +1409,7 @@ type SchedulerConfigSpec struct { func (x *SchedulerConfigSpec) Reset() { *x = SchedulerConfigSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[18] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1366,7 +1422,7 @@ func (x *SchedulerConfigSpec) String() string { func (*SchedulerConfigSpec) ProtoMessage() {} func (x *SchedulerConfigSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[18] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1379,7 +1435,7 @@ func (x *SchedulerConfigSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use SchedulerConfigSpec.ProtoReflect.Descriptor instead. func (*SchedulerConfigSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{18} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{19} } func (x *SchedulerConfigSpec) GetEnabled() bool { @@ -1430,7 +1486,7 @@ type SecretsStatusSpec struct { func (x *SecretsStatusSpec) Reset() { *x = SecretsStatusSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[19] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1443,7 +1499,7 @@ func (x *SecretsStatusSpec) String() string { func (*SecretsStatusSpec) ProtoMessage() {} func (x *SecretsStatusSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[19] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1456,7 +1512,7 @@ func (x *SecretsStatusSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretsStatusSpec.ProtoReflect.Descriptor instead. func (*SecretsStatusSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{19} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{20} } func (x *SecretsStatusSpec) GetReady() bool { @@ -1485,7 +1541,7 @@ type SingleManifest struct { func (x *SingleManifest) Reset() { *x = SingleManifest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[20] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1498,7 +1554,7 @@ func (x *SingleManifest) String() string { func (*SingleManifest) ProtoMessage() {} func (x *SingleManifest) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[20] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1511,7 +1567,7 @@ func (x *SingleManifest) ProtoReflect() protoreflect.Message { // Deprecated: Use SingleManifest.ProtoReflect.Descriptor instead. func (*SingleManifest) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{20} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{21} } func (x *SingleManifest) GetObject() *structpb.Struct { @@ -1533,7 +1589,7 @@ type StaticPodServerStatusSpec struct { func (x *StaticPodServerStatusSpec) Reset() { *x = StaticPodServerStatusSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[21] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1546,7 +1602,7 @@ func (x *StaticPodServerStatusSpec) String() string { func (*StaticPodServerStatusSpec) ProtoMessage() {} func (x *StaticPodServerStatusSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[21] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1559,7 +1615,7 @@ func (x *StaticPodServerStatusSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticPodServerStatusSpec.ProtoReflect.Descriptor instead. func (*StaticPodServerStatusSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{21} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{22} } func (x *StaticPodServerStatusSpec) GetUrl() string { @@ -1581,7 +1637,7 @@ type StaticPodSpec struct { func (x *StaticPodSpec) Reset() { *x = StaticPodSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[22] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1594,7 +1650,7 @@ func (x *StaticPodSpec) String() string { func (*StaticPodSpec) ProtoMessage() {} func (x *StaticPodSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[22] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1607,7 +1663,7 @@ func (x *StaticPodSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticPodSpec.ProtoReflect.Descriptor instead. func (*StaticPodSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{22} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{23} } func (x *StaticPodSpec) GetPod() *structpb.Struct { @@ -1629,7 +1685,7 @@ type StaticPodStatusSpec struct { func (x *StaticPodStatusSpec) Reset() { *x = StaticPodStatusSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[23] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1642,7 +1698,7 @@ func (x *StaticPodStatusSpec) String() string { func (*StaticPodStatusSpec) ProtoMessage() {} func (x *StaticPodStatusSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[23] + mi := &file_resource_definitions_k8s_k8s_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1655,7 +1711,7 @@ func (x *StaticPodStatusSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticPodStatusSpec.ProtoReflect.Descriptor instead. func (*StaticPodStatusSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{23} + return file_resource_definitions_k8s_k8s_proto_rawDescGZIP(), []int{24} } func (x *StaticPodStatusSpec) GetPodStatus() *structpb.Struct { @@ -1945,70 +2001,74 @@ var file_resource_definitions_k8s_k8s_proto_rawDesc = []byte{ 0x6f, 0x64, 0x65, 0x49, 0x50, 0x53, 0x70, 0x65, 0x63, 0x12, 0x2b, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x49, 0x50, 0x52, 0x09, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, 0x55, 0x0a, 0x0c, 0x4e, 0x6f, 0x64, 0x65, 0x6e, 0x61, - 0x6d, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x04, - 0x0a, 0x13, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x53, 0x70, 0x65, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, - 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x61, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x61, - 0x72, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x74, 0x61, 0x6c, 0x6f, - 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x45, - 0x78, 0x74, 0x72, 0x61, 0x41, 0x72, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x65, - 0x78, 0x74, 0x72, 0x61, 0x41, 0x72, 0x67, 0x73, 0x12, 0x50, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x72, - 0x61, 0x5f, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x2b, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, - 0x2e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x0c, 0x65, 0x78, - 0x74, 0x72, 0x61, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x82, 0x01, 0x0a, 0x15, 0x65, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4d, 0x2e, 0x74, 0x61, 0x6c, - 0x6f, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, 0x66, 0x69, - 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, - 0x64, 0x75, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x2e, - 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x1a, - 0x3c, 0x0a, 0x0e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x41, 0x72, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x47, 0x0a, - 0x19, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x43, 0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x14, 0x0a, 0x05, 0x72, - 0x65, 0x61, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, 0x64, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x0e, 0x53, - 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, - 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x2d, - 0x0a, 0x19, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x50, 0x6f, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x0a, - 0x0d, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x50, 0x6f, 0x64, 0x53, 0x70, 0x65, 0x63, 0x12, 0x29, - 0x0a, 0x03, 0x70, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x52, 0x03, 0x70, 0x6f, 0x64, 0x22, 0x4d, 0x0a, 0x13, 0x53, 0x74, 0x61, - 0x74, 0x69, 0x63, 0x50, 0x6f, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, - 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x6f, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x09, 0x70, - 0x6f, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, - 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x6b, - 0x38, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x11, 0x4e, 0x6f, 0x64, 0x65, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x53, 0x70, 0x65, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0x55, 0x0a, 0x0c, 0x4e, 0x6f, 0x64, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x53, + 0x70, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x29, 0x0a, 0x10, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x04, 0x0a, 0x13, 0x53, + 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, + 0x65, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x12, 0x61, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x61, 0x72, 0x67, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x45, 0x78, 0x74, 0x72, + 0x61, 0x41, 0x72, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x65, 0x78, 0x74, 0x72, + 0x61, 0x41, 0x72, 0x67, 0x73, 0x12, 0x50, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, + 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, + 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x45, 0x78, + 0x74, 0x72, 0x61, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x72, 0x61, + 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x82, 0x01, 0x0a, 0x15, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4d, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x45, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x1a, 0x3c, 0x0a, 0x0e, + 0x45, 0x78, 0x74, 0x72, 0x61, 0x41, 0x72, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x47, 0x0a, 0x19, 0x45, 0x6e, + 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x43, 0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x61, 0x64, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x0e, 0x53, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x6f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x2d, 0x0a, 0x19, 0x53, + 0x74, 0x61, 0x74, 0x69, 0x63, 0x50, 0x6f, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x0a, 0x0d, 0x53, 0x74, + 0x61, 0x74, 0x69, 0x63, 0x50, 0x6f, 0x64, 0x53, 0x70, 0x65, 0x63, 0x12, 0x29, 0x0a, 0x03, 0x70, + 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x52, 0x03, 0x70, 0x6f, 0x64, 0x22, 0x4d, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, + 0x50, 0x6f, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x36, 0x0a, + 0x0a, 0x70, 0x6f, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x09, 0x70, 0x6f, 0x64, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, + 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, + 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x6b, 0x38, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2023,7 +2083,7 @@ func file_resource_definitions_k8s_k8s_proto_rawDescGZIP() []byte { return file_resource_definitions_k8s_k8s_proto_rawDescData } -var file_resource_definitions_k8s_k8s_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_resource_definitions_k8s_k8s_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_resource_definitions_k8s_k8s_proto_goTypes = []interface{}{ (*APIServerConfigSpec)(nil), // 0: talos.resource.definitions.k8s.APIServerConfigSpec (*AdmissionControlConfigSpec)(nil), // 1: talos.resource.definitions.k8s.AdmissionControlConfigSpec @@ -2042,51 +2102,52 @@ var file_resource_definitions_k8s_k8s_proto_goTypes = []interface{}{ (*ManifestStatusSpec)(nil), // 14: talos.resource.definitions.k8s.ManifestStatusSpec (*NodeIPConfigSpec)(nil), // 15: talos.resource.definitions.k8s.NodeIPConfigSpec (*NodeIPSpec)(nil), // 16: talos.resource.definitions.k8s.NodeIPSpec - (*NodenameSpec)(nil), // 17: talos.resource.definitions.k8s.NodenameSpec - (*SchedulerConfigSpec)(nil), // 18: talos.resource.definitions.k8s.SchedulerConfigSpec - (*SecretsStatusSpec)(nil), // 19: talos.resource.definitions.k8s.SecretsStatusSpec - (*SingleManifest)(nil), // 20: talos.resource.definitions.k8s.SingleManifest - (*StaticPodServerStatusSpec)(nil), // 21: talos.resource.definitions.k8s.StaticPodServerStatusSpec - (*StaticPodSpec)(nil), // 22: talos.resource.definitions.k8s.StaticPodSpec - (*StaticPodStatusSpec)(nil), // 23: talos.resource.definitions.k8s.StaticPodStatusSpec - nil, // 24: talos.resource.definitions.k8s.APIServerConfigSpec.ExtraArgsEntry - nil, // 25: talos.resource.definitions.k8s.APIServerConfigSpec.EnvironmentVariablesEntry - nil, // 26: talos.resource.definitions.k8s.ControllerManagerConfigSpec.ExtraArgsEntry - nil, // 27: talos.resource.definitions.k8s.ControllerManagerConfigSpec.EnvironmentVariablesEntry - nil, // 28: talos.resource.definitions.k8s.ExtraManifest.ExtraHeadersEntry - nil, // 29: talos.resource.definitions.k8s.KubeletConfigSpec.ExtraArgsEntry - nil, // 30: talos.resource.definitions.k8s.SchedulerConfigSpec.ExtraArgsEntry - nil, // 31: talos.resource.definitions.k8s.SchedulerConfigSpec.EnvironmentVariablesEntry - (*structpb.Struct)(nil), // 32: google.protobuf.Struct - (*common.NetIP)(nil), // 33: common.NetIP - (*proto.Mount)(nil), // 34: talos.resource.definitions.proto.Mount + (*NodeLabelSpecSpec)(nil), // 17: talos.resource.definitions.k8s.NodeLabelSpecSpec + (*NodenameSpec)(nil), // 18: talos.resource.definitions.k8s.NodenameSpec + (*SchedulerConfigSpec)(nil), // 19: talos.resource.definitions.k8s.SchedulerConfigSpec + (*SecretsStatusSpec)(nil), // 20: talos.resource.definitions.k8s.SecretsStatusSpec + (*SingleManifest)(nil), // 21: talos.resource.definitions.k8s.SingleManifest + (*StaticPodServerStatusSpec)(nil), // 22: talos.resource.definitions.k8s.StaticPodServerStatusSpec + (*StaticPodSpec)(nil), // 23: talos.resource.definitions.k8s.StaticPodSpec + (*StaticPodStatusSpec)(nil), // 24: talos.resource.definitions.k8s.StaticPodStatusSpec + nil, // 25: talos.resource.definitions.k8s.APIServerConfigSpec.ExtraArgsEntry + nil, // 26: talos.resource.definitions.k8s.APIServerConfigSpec.EnvironmentVariablesEntry + nil, // 27: talos.resource.definitions.k8s.ControllerManagerConfigSpec.ExtraArgsEntry + nil, // 28: talos.resource.definitions.k8s.ControllerManagerConfigSpec.EnvironmentVariablesEntry + nil, // 29: talos.resource.definitions.k8s.ExtraManifest.ExtraHeadersEntry + nil, // 30: talos.resource.definitions.k8s.KubeletConfigSpec.ExtraArgsEntry + nil, // 31: talos.resource.definitions.k8s.SchedulerConfigSpec.ExtraArgsEntry + nil, // 32: talos.resource.definitions.k8s.SchedulerConfigSpec.EnvironmentVariablesEntry + (*structpb.Struct)(nil), // 33: google.protobuf.Struct + (*common.NetIP)(nil), // 34: common.NetIP + (*proto.Mount)(nil), // 35: talos.resource.definitions.proto.Mount } var file_resource_definitions_k8s_k8s_proto_depIdxs = []int32{ - 24, // 0: talos.resource.definitions.k8s.APIServerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.APIServerConfigSpec.ExtraArgsEntry + 25, // 0: talos.resource.definitions.k8s.APIServerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.APIServerConfigSpec.ExtraArgsEntry 10, // 1: talos.resource.definitions.k8s.APIServerConfigSpec.extra_volumes:type_name -> talos.resource.definitions.k8s.ExtraVolume - 25, // 2: talos.resource.definitions.k8s.APIServerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.APIServerConfigSpec.EnvironmentVariablesEntry + 26, // 2: talos.resource.definitions.k8s.APIServerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.APIServerConfigSpec.EnvironmentVariablesEntry 2, // 3: talos.resource.definitions.k8s.AdmissionControlConfigSpec.config:type_name -> talos.resource.definitions.k8s.AdmissionPluginSpec - 32, // 4: talos.resource.definitions.k8s.AdmissionPluginSpec.configuration:type_name -> google.protobuf.Struct - 32, // 5: talos.resource.definitions.k8s.AuditPolicyConfigSpec.config:type_name -> google.protobuf.Struct - 26, // 6: talos.resource.definitions.k8s.ControllerManagerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.ControllerManagerConfigSpec.ExtraArgsEntry + 33, // 4: talos.resource.definitions.k8s.AdmissionPluginSpec.configuration:type_name -> google.protobuf.Struct + 33, // 5: talos.resource.definitions.k8s.AuditPolicyConfigSpec.config:type_name -> google.protobuf.Struct + 27, // 6: talos.resource.definitions.k8s.ControllerManagerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.ControllerManagerConfigSpec.ExtraArgsEntry 10, // 7: talos.resource.definitions.k8s.ControllerManagerConfigSpec.extra_volumes:type_name -> talos.resource.definitions.k8s.ExtraVolume - 27, // 8: talos.resource.definitions.k8s.ControllerManagerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.ControllerManagerConfigSpec.EnvironmentVariablesEntry - 33, // 9: talos.resource.definitions.k8s.EndpointSpec.addresses:type_name -> common.NetIP - 28, // 10: talos.resource.definitions.k8s.ExtraManifest.extra_headers:type_name -> talos.resource.definitions.k8s.ExtraManifest.ExtraHeadersEntry + 28, // 8: talos.resource.definitions.k8s.ControllerManagerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.ControllerManagerConfigSpec.EnvironmentVariablesEntry + 34, // 9: talos.resource.definitions.k8s.EndpointSpec.addresses:type_name -> common.NetIP + 29, // 10: talos.resource.definitions.k8s.ExtraManifest.extra_headers:type_name -> talos.resource.definitions.k8s.ExtraManifest.ExtraHeadersEntry 8, // 11: talos.resource.definitions.k8s.ExtraManifestsConfigSpec.extra_manifests:type_name -> talos.resource.definitions.k8s.ExtraManifest - 29, // 12: talos.resource.definitions.k8s.KubeletConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.KubeletConfigSpec.ExtraArgsEntry - 34, // 13: talos.resource.definitions.k8s.KubeletConfigSpec.extra_mounts:type_name -> talos.resource.definitions.proto.Mount - 32, // 14: talos.resource.definitions.k8s.KubeletConfigSpec.extra_config:type_name -> google.protobuf.Struct - 34, // 15: talos.resource.definitions.k8s.KubeletSpecSpec.extra_mounts:type_name -> talos.resource.definitions.proto.Mount - 32, // 16: talos.resource.definitions.k8s.KubeletSpecSpec.config:type_name -> google.protobuf.Struct - 20, // 17: talos.resource.definitions.k8s.ManifestSpec.items:type_name -> talos.resource.definitions.k8s.SingleManifest - 33, // 18: talos.resource.definitions.k8s.NodeIPSpec.addresses:type_name -> common.NetIP - 30, // 19: talos.resource.definitions.k8s.SchedulerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.SchedulerConfigSpec.ExtraArgsEntry + 30, // 12: talos.resource.definitions.k8s.KubeletConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.KubeletConfigSpec.ExtraArgsEntry + 35, // 13: talos.resource.definitions.k8s.KubeletConfigSpec.extra_mounts:type_name -> talos.resource.definitions.proto.Mount + 33, // 14: talos.resource.definitions.k8s.KubeletConfigSpec.extra_config:type_name -> google.protobuf.Struct + 35, // 15: talos.resource.definitions.k8s.KubeletSpecSpec.extra_mounts:type_name -> talos.resource.definitions.proto.Mount + 33, // 16: talos.resource.definitions.k8s.KubeletSpecSpec.config:type_name -> google.protobuf.Struct + 21, // 17: talos.resource.definitions.k8s.ManifestSpec.items:type_name -> talos.resource.definitions.k8s.SingleManifest + 34, // 18: talos.resource.definitions.k8s.NodeIPSpec.addresses:type_name -> common.NetIP + 31, // 19: talos.resource.definitions.k8s.SchedulerConfigSpec.extra_args:type_name -> talos.resource.definitions.k8s.SchedulerConfigSpec.ExtraArgsEntry 10, // 20: talos.resource.definitions.k8s.SchedulerConfigSpec.extra_volumes:type_name -> talos.resource.definitions.k8s.ExtraVolume - 31, // 21: talos.resource.definitions.k8s.SchedulerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.SchedulerConfigSpec.EnvironmentVariablesEntry - 32, // 22: talos.resource.definitions.k8s.SingleManifest.object:type_name -> google.protobuf.Struct - 32, // 23: talos.resource.definitions.k8s.StaticPodSpec.pod:type_name -> google.protobuf.Struct - 32, // 24: talos.resource.definitions.k8s.StaticPodStatusSpec.pod_status:type_name -> google.protobuf.Struct + 32, // 21: talos.resource.definitions.k8s.SchedulerConfigSpec.environment_variables:type_name -> talos.resource.definitions.k8s.SchedulerConfigSpec.EnvironmentVariablesEntry + 33, // 22: talos.resource.definitions.k8s.SingleManifest.object:type_name -> google.protobuf.Struct + 33, // 23: talos.resource.definitions.k8s.StaticPodSpec.pod:type_name -> google.protobuf.Struct + 33, // 24: talos.resource.definitions.k8s.StaticPodStatusSpec.pod_status:type_name -> google.protobuf.Struct 25, // [25:25] is the sub-list for method output_type 25, // [25:25] is the sub-list for method input_type 25, // [25:25] is the sub-list for extension type_name @@ -2305,7 +2366,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NodenameSpec); i { + switch v := v.(*NodeLabelSpecSpec); i { case 0: return &v.state case 1: @@ -2317,7 +2378,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SchedulerConfigSpec); i { + switch v := v.(*NodenameSpec); i { case 0: return &v.state case 1: @@ -2329,7 +2390,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SecretsStatusSpec); i { + switch v := v.(*SchedulerConfigSpec); i { case 0: return &v.state case 1: @@ -2341,7 +2402,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SingleManifest); i { + switch v := v.(*SecretsStatusSpec); i { case 0: return &v.state case 1: @@ -2353,7 +2414,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StaticPodServerStatusSpec); i { + switch v := v.(*SingleManifest); i { case 0: return &v.state case 1: @@ -2365,7 +2426,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StaticPodSpec); i { + switch v := v.(*StaticPodServerStatusSpec); i { case 0: return &v.state case 1: @@ -2377,6 +2438,18 @@ func file_resource_definitions_k8s_k8s_proto_init() { } } file_resource_definitions_k8s_k8s_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StaticPodSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_definitions_k8s_k8s_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StaticPodStatusSpec); i { case 0: return &v.state @@ -2395,7 +2468,7 @@ func file_resource_definitions_k8s_k8s_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_definitions_k8s_k8s_proto_rawDesc, NumEnums: 0, - NumMessages: 32, + NumMessages: 33, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go b/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go index e338eeb16..1f587df7a 100644 --- a/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go @@ -1373,6 +1373,53 @@ func (m *NodeIPSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *NodeLabelSpecSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NodeLabelSpecSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *NodeLabelSpecSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Value) > 0 { + i -= len(m.Value) + copy(dAtA[i:], m.Value) + i = encodeVarint(dAtA, i, uint64(len(m.Value))) + i-- + dAtA[i] = 0x12 + } + if len(m.Key) > 0 { + i -= len(m.Key) + copy(dAtA[i:], m.Key) + i = encodeVarint(dAtA, i, uint64(len(m.Key))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *NodenameSpec) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -2376,6 +2423,26 @@ func (m *NodeIPSpec) SizeVT() (n int) { return n } +func (m *NodeLabelSpecSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Key) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + l = len(m.Value) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + if m.unknownFields != nil { + n += len(m.unknownFields) + } + return n +} + func (m *NodenameSpec) SizeVT() (n int) { if m == nil { return 0 @@ -6214,6 +6281,121 @@ func (m *NodeIPSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *NodeLabelSpecSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NodeLabelSpecSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NodeLabelSpecSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Key = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Value = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *NodenameSpec) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/pkg/machinery/config/provider.go b/pkg/machinery/config/provider.go index a7ddaa42c..411a2bcc6 100644 --- a/pkg/machinery/config/provider.go +++ b/pkg/machinery/config/provider.go @@ -66,6 +66,7 @@ type MachineConfig interface { Logging() Logging Kernel() Kernel SeccompProfiles() []SeccompProfile + NodeLabels() NodeLabels } // SeccompProfile defines the requirements for a config that pertains to seccomp @@ -75,6 +76,9 @@ type SeccompProfile interface { Value() map[string]interface{} } +// NodeLabels defines the labels that should be set on a node. +type NodeLabels map[string]string + // Disk represents the options available for partitioning, formatting, and // mounting extra disks. type Disk interface { diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go index a71e4a32f..b31bb186a 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go @@ -68,6 +68,11 @@ func (m *MachineSeccompProfile) Value() map[string]interface{} { return m.MachineSeccompProfileValue.Object } +// NodeLabels implements the config.Provider interface. +func (m *MachineConfig) NodeLabels() config.NodeLabels { + return m.MachineNodeLabels +} + // Cluster implements the config.Provider interface. func (c *Config) Cluster() config.ClusterConfig { if c.ClusterConfig == nil { diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go index 1b5a60c58..7d9ffa08f 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go @@ -830,6 +830,12 @@ type MachineConfig struct { // examples: // - value: machineSeccompExample MachineSeccompProfiles []*MachineSeccompProfile `yaml:"seccompProfiles,omitempty" talos:"omitonlyifnil"` + // description: | + // Configures the node labels for the machine. + // examples: + // - name: node labels example. + // value: 'map[string]string{"exampleLabel": "exampleLabelValue"}' + MachineNodeLabels map[string]string `yaml:"nodeLabels,omitempty"` } // MachineSeccompProfile defines seccomp profiles for the machine. diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go index 1bc43fe57..324b7c632 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go @@ -145,7 +145,7 @@ func init() { FieldName: "machine", }, } - MachineConfigDoc.Fields = make([]encoder.Doc, 22) + MachineConfigDoc.Fields = make([]encoder.Doc, 23) MachineConfigDoc.Fields[0].Name = "type" MachineConfigDoc.Fields[0].Type = "string" MachineConfigDoc.Fields[0].Note = "" @@ -313,6 +313,13 @@ func init() { MachineConfigDoc.Fields[21].Comments[encoder.LineComment] = "Configures the seccomp profiles for the machine." MachineConfigDoc.Fields[21].AddExample("", machineSeccompExample) + MachineConfigDoc.Fields[22].Name = "nodeLabels" + MachineConfigDoc.Fields[22].Type = "map[string]string" + MachineConfigDoc.Fields[22].Note = "" + MachineConfigDoc.Fields[22].Description = "Configures the node labels for the machine." + MachineConfigDoc.Fields[22].Comments[encoder.LineComment] = "Configures the node labels for the machine." + + MachineConfigDoc.Fields[22].AddExample("node labels example.", map[string]string{"exampleLabel": "exampleLabelValue"}) MachineSeccompProfileDoc.Type = "MachineSeccompProfile" MachineSeccompProfileDoc.Comments[encoder.LineComment] = "MachineSeccompProfile defines seccomp profiles for the machine." diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go index 2495b6439..acd7b789d 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go @@ -24,6 +24,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/machine" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/kubelet" + "github.com/siderolabs/talos/pkg/machinery/labels" "github.com/siderolabs/talos/pkg/machinery/nethelpers" ) @@ -257,6 +258,10 @@ func (c *Config) Validate(mode config.RuntimeMode, options ...config.ValidationO } } + if err := labels.Validate(c.MachineConfig.MachineNodeLabels); err != nil { + result = multierror.Append(result, fmt.Errorf("invalid machine node labels: %w", err)) + } + if c.Machine().Features().KubernetesTalosAPIAccess().Enabled() && !c.Machine().Features().RBACEnabled() { result = multierror.Append(result, fmt.Errorf("feature API RBAC should be enabled when Kubernetes Talos API Access feature is enabled")) } diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go index 091461668..e95639204 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go @@ -7,6 +7,7 @@ package v1alpha1_test import ( "fmt" "net/url" + "strings" "testing" "github.com/siderolabs/crypto/x509" @@ -1198,6 +1199,31 @@ func TestValidate(t *testing.T) { }, expectedError: "1 error occurred:\n\t* feature Kubernetes Talos API Access can only be enabled on control plane machines\n\n", }, + { + name: "NodeLabels", + config: &v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineType: "worker", + MachineNodeLabels: map[string]string{ + "/foo": "bar", + "key": "value", + "talos.dev/foo": "bar", + "@!": "#$", + "123@.dev/": "456", + "a/b/c": strings.Repeat("a", 64), + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + endpointURL, + }, + }, + }, + }, + expectedError: "1 error occurred:\n\t* invalid machine node labels: 6 errors occurred:\n\t* prefix cannot be empty: \"/foo\"\n\t* prefix \"123@.dev\" is invalid: domain doesn't match required format: \"123@.dev\"\n\t* name \"@!\" is invalid\n\t* label value \"#$\" is invalid\n\t* invalid format: too many slashes: \"a/b/c\"\n\t* label value length exceeds limit of 63: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\n\n\n", //nolint:lll + }, } { test := test diff --git a/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go b/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go index 0827b33c8..47f167241 100644 --- a/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go @@ -1410,6 +1410,13 @@ func (in *MachineConfig) DeepCopyInto(out *MachineConfig) { } } } + if in.MachineNodeLabels != nil { + in, out := &in.MachineNodeLabels, &out.MachineNodeLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index f2ecfa691..fcf513d23 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -603,6 +603,9 @@ const ( // AnnotationStaticPodConfigFileVersion is the annotation key for the static pod configuration file version. AnnotationStaticPodConfigFileVersion = "talos.dev/config-file-version" + // AnnotationOwnedLabels is the annotation key for the list of node labels owned by Talos. + AnnotationOwnedLabels = "talos.dev/owned-labels" + // DefaultNTPServer is the NTP server to use if not configured explicitly. // // TODO: Once we get naming sorted we need to apply for a project specific address diff --git a/pkg/machinery/labels/validate.go b/pkg/machinery/labels/validate.go new file mode 100644 index 000000000..51e9d9ca7 --- /dev/null +++ b/pkg/machinery/labels/validate.go @@ -0,0 +1,132 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package labels contains adapter label validation functions from Kubernetes. +// +// We want to avoid dependency of machinery on Kubernetes packages. +package labels + +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/siderolabs/gen/maps" +) + +// Validate validates that a set of labels are correctly defined. +func Validate(labels map[string]string) error { + var multiErr *multierror.Error + + keys := maps.Keys(labels) + sort.Strings(keys) + + for _, k := range keys { + if err := ValidateQualifiedName(k); err != nil { + multiErr = multierror.Append(multiErr, err) + } + + if err := ValidateLabelValue(labels[k]); err != nil { + multiErr = multierror.Append(multiErr, err) + } + } + + return multiErr.ErrorOrNil() +} + +const ( + dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" + dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" + dns1123SubdomainMaxLength int = 253 +) + +var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") + +// ValidateDNS1123Subdomain tests for a string that conforms to the definition of a +// subdomain in DNS (RFC 1123). +func ValidateDNS1123Subdomain(value string) error { + if len(value) > dns1123SubdomainMaxLength { + return fmt.Errorf("domain name length exceeds limit of %d: %q", dns1123SubdomainMaxLength, value) + } + + if !dns1123SubdomainRegexp.MatchString(value) { + return fmt.Errorf("domain doesn't match required format: %q", value) + } + + return nil +} + +const ( + qnameCharFmt string = "[A-Za-z0-9]" + qnameExtCharFmt string = "[-A-Za-z0-9_.]" + qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt + qualifiedNameMaxLength int = 63 +) + +var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$") + +// ValidateQualifiedName tests whether the value passed is what Kubernetes calls a +// "qualified name". +// +// This is a format used in various places throughout the +// system. If the value is not valid, a list of error strings is returned. +// Otherwise an empty list (or nil) is returned. +func ValidateQualifiedName(value string) error { + parts := strings.Split(value, "/") + + var name string + + switch len(parts) { + case 1: + name = parts[0] + case 2: + var prefix string + + prefix, name = parts[0], parts[1] + + if len(prefix) == 0 { + return fmt.Errorf("prefix cannot be empty: %q", value) + } else if err := ValidateDNS1123Subdomain(prefix); err != nil { + return fmt.Errorf("prefix %q is invalid: %v", prefix, err) + } + default: + return fmt.Errorf("invalid format: too many slashes: %q", value) + } + + switch { + case len(name) == 0: + return fmt.Errorf("name cannot be empty: %q", value) + case len(name) > qualifiedNameMaxLength: + return fmt.Errorf("name is too long: %q (limit is %d)", value, qualifiedNameMaxLength) + case !qualifiedNameRegexp.MatchString(name): + return fmt.Errorf("name %q is invalid", name) + } + + return nil +} + +const ( + labelValueFmt string = "(" + qualifiedNameFmt + ")?" + labelValueMaxLength int = 63 +) + +var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$") + +// ValidateLabelValue tests whether the value passed is a valid label value. +// +// If the value is not valid, a list of error strings is returned. +// Otherwise an empty list (or nil) is returned. +func ValidateLabelValue(value string) error { + if len(value) > labelValueMaxLength { + return fmt.Errorf("label value length exceeds limit of %d: %q", labelValueMaxLength, value) + } + + if !labelValueRegexp.MatchString(value) { + return fmt.Errorf("label value %q is invalid", value) + } + + return nil +} diff --git a/pkg/machinery/labels/validate_test.go b/pkg/machinery/labels/validate_test.go new file mode 100644 index 000000000..0d9833dd1 --- /dev/null +++ b/pkg/machinery/labels/validate_test.go @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package labels_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/talos/pkg/machinery/labels" +) + +func TestValidate(t *testing.T) { + for _, tt := range []struct { + name string + labels map[string]string + + expectedError string + }{ + { + name: "empty", + }, + { + name: "valid", + labels: map[string]string{ + "talos.dev/label": "value", + "foo": "bar", + "kubernetes.io/hostname": "hostname1", + }, + }, + { + name: "invalid", + labels: map[string]string{ + "345@.345/label": "value", + "foo_": "bar", + "/foo": "bar", + "a/b/c": "bar", + "kubernetes.io/hostname": "hostname1_", + strings.Repeat("a", 64): "bar", + "bar": strings.Repeat("a", 64), + }, + expectedError: "7 errors occurred:\n\t* prefix cannot be empty: \"/foo\"\n\t* prefix \"345@.345\" is invalid: domain doesn't match required format: \"345@.345\"\n\t* invalid format: too many slashes: \"a/b/c\"\n\t* name is too long: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" (limit is 63)\n\t* label value length exceeds limit of 63: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\t* name \"foo_\" is invalid\n\t* label value \"hostname1_\" is invalid\n\n", //nolint:lll + }, + } { + t.Run(tt.name, func(t *testing.T) { + err := labels.Validate(tt.labels) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/machinery/resources/k8s/admissioncontrol_config.go b/pkg/machinery/resources/k8s/admissioncontrol_config.go index 6fbb25f3d..9ad3f936f 100644 --- a/pkg/machinery/resources/k8s/admissioncontrol_config.go +++ b/pkg/machinery/resources/k8s/admissioncontrol_config.go @@ -16,9 +16,6 @@ import ( "github.com/siderolabs/talos/pkg/machinery/proto" ) -//nolint:lll -//go:generate deep-copy -type AdmissionControlConfigSpec -type APIServerConfigSpec -type AuditPolicyConfigSpec -type ConfigStatusSpec -type ControllerManagerConfigSpec -type EndpointSpec -type ExtraManifestsConfigSpec -type KubeletLifecycleSpec -type KubeletSpecSpec -type ManifestSpec -type ManifestStatusSpec -type BootstrapManifestsConfigSpec -type KubeletConfigSpec -type NodeIPSpec -type NodeIPConfigSpec -type NodenameSpec -type SchedulerConfigSpec -type SecretsStatusSpec -type StaticPodSpec -type StaticPodStatusSpec -type StaticPodServerStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . - // AdmissionControlConfigType is type of AdmissionControlConfig resource. const AdmissionControlConfigType = resource.Type("AdmissionControlConfigs.kubernetes.talos.dev") diff --git a/pkg/machinery/resources/k8s/deep_copy.generated.go b/pkg/machinery/resources/k8s/deep_copy.generated.go index 54c96661e..3a70f1af1 100644 --- a/pkg/machinery/resources/k8s/deep_copy.generated.go +++ b/pkg/machinery/resources/k8s/deep_copy.generated.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "deep-copy -type AdmissionControlConfigSpec -type APIServerConfigSpec -type AuditPolicyConfigSpec -type ConfigStatusSpec -type ControllerManagerConfigSpec -type EndpointSpec -type ExtraManifestsConfigSpec -type KubeletLifecycleSpec -type KubeletSpecSpec -type ManifestSpec -type ManifestStatusSpec -type BootstrapManifestsConfigSpec -type KubeletConfigSpec -type NodeIPSpec -type NodeIPConfigSpec -type NodenameSpec -type SchedulerConfigSpec -type SecretsStatusSpec -type StaticPodSpec -type StaticPodStatusSpec -type StaticPodServerStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type AdmissionControlConfigSpec -type APIServerConfigSpec -type AuditPolicyConfigSpec -type BootstrapManifestsConfigSpec -type ConfigStatusSpec -type ControllerManagerConfigSpec -type EndpointSpec -type ExtraManifestsConfigSpec -type KubeletLifecycleSpec -type KubeletSpecSpec -type ManifestSpec -type ManifestStatusSpec -type NodeLabelSpecSpec -type KubeletConfigSpec -type NodeIPSpec -type NodeIPConfigSpec -type NodenameSpec -type SchedulerConfigSpec -type SecretsStatusSpec -type StaticPodSpec -type StaticPodStatusSpec -type StaticPodServerStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package k8s @@ -72,6 +72,20 @@ func (o AuditPolicyConfigSpec) DeepCopy() AuditPolicyConfigSpec { return cp } +// DeepCopy generates a deep copy of BootstrapManifestsConfigSpec. +func (o BootstrapManifestsConfigSpec) DeepCopy() BootstrapManifestsConfigSpec { + var cp BootstrapManifestsConfigSpec = o + if o.PodCIDRs != nil { + cp.PodCIDRs = make([]string, len(o.PodCIDRs)) + copy(cp.PodCIDRs, o.PodCIDRs) + } + if o.ProxyArgs != nil { + cp.ProxyArgs = make([]string, len(o.ProxyArgs)) + copy(cp.ProxyArgs, o.ProxyArgs) + } + return cp +} + // DeepCopy generates a deep copy of ConfigStatusSpec. func (o ConfigStatusSpec) DeepCopy() ConfigStatusSpec { var cp ConfigStatusSpec = o @@ -196,17 +210,9 @@ func (o ManifestStatusSpec) DeepCopy() ManifestStatusSpec { return cp } -// DeepCopy generates a deep copy of BootstrapManifestsConfigSpec. -func (o BootstrapManifestsConfigSpec) DeepCopy() BootstrapManifestsConfigSpec { - var cp BootstrapManifestsConfigSpec = o - if o.PodCIDRs != nil { - cp.PodCIDRs = make([]string, len(o.PodCIDRs)) - copy(cp.PodCIDRs, o.PodCIDRs) - } - if o.ProxyArgs != nil { - cp.ProxyArgs = make([]string, len(o.ProxyArgs)) - copy(cp.ProxyArgs, o.ProxyArgs) - } +// DeepCopy generates a deep copy of NodeLabelSpecSpec. +func (o NodeLabelSpecSpec) DeepCopy() NodeLabelSpecSpec { + var cp NodeLabelSpecSpec = o return cp } diff --git a/pkg/machinery/resources/k8s/k8s.go b/pkg/machinery/resources/k8s/k8s.go index d397474af..6192d5787 100644 --- a/pkg/machinery/resources/k8s/k8s.go +++ b/pkg/machinery/resources/k8s/k8s.go @@ -7,6 +7,9 @@ package k8s import "github.com/cosi-project/runtime/pkg/resource" +//nolint:lll +//go:generate deep-copy -type AdmissionControlConfigSpec -type APIServerConfigSpec -type AuditPolicyConfigSpec -type BootstrapManifestsConfigSpec -type ConfigStatusSpec -type ControllerManagerConfigSpec -type EndpointSpec -type ExtraManifestsConfigSpec -type KubeletLifecycleSpec -type KubeletSpecSpec -type ManifestSpec -type ManifestStatusSpec -type NodeLabelSpecSpec -type KubeletConfigSpec -type NodeIPSpec -type NodeIPConfigSpec -type NodenameSpec -type SchedulerConfigSpec -type SecretsStatusSpec -type StaticPodSpec -type StaticPodStatusSpec -type StaticPodServerStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . + // NamespaceName contains resources supporting Kubernetes components on all node types. const NamespaceName resource.Namespace = "k8s" diff --git a/pkg/machinery/resources/k8s/node_label_spec.go b/pkg/machinery/resources/k8s/node_label_spec.go new file mode 100644 index 000000000..e16b9b56c --- /dev/null +++ b/pkg/machinery/resources/k8s/node_label_spec.go @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package k8s + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/talos/pkg/machinery/proto" +) + +// NodeLabelSpecType is the type. +const NodeLabelSpecType = resource.Type("NodeLabelSpecs.k8s.talos.dev") + +// NodeLabelSpecSpec represents a label that's attached to a Talos node. +// +//gotagsrewrite:gen +type NodeLabelSpecSpec struct { + Key string `yaml:"key" protobuf:"1"` + Value string `yaml:"value" protobuf:"2"` +} + +// NodeLabelSpec ... +type NodeLabelSpec = typed.Resource[NodeLabelSpecSpec, NodeLabelSpecRD] + +// NewNodeLabelSpec initializes a NodeLabel resource. +func NewNodeLabelSpec(id resource.ID) *NodeLabelSpec { + return typed.NewResource[NodeLabelSpecSpec, NodeLabelSpecRD]( + resource.NewMetadata(NamespaceName, NodeLabelSpecType, id, resource.VersionUndefined), + NodeLabelSpecSpec{}, + ) +} + +// NewPopulatedNodeLabelSpec initializes a NodeLabel resource and populates it. +func NewPopulatedNodeLabelSpec(id resource.ID, key, value string, createdByTalos bool) *NodeLabelSpec { + return typed.NewResource[NodeLabelSpecSpec, NodeLabelSpecRD]( + resource.NewMetadata(NamespaceName, NodeLabelSpecType, id, resource.VersionUndefined), + NodeLabelSpecSpec{ + Key: key, + Value: value, + }, + ) +} + +// NodeLabelSpecRD provides auxiliary methods for NodeLabel. +type NodeLabelSpecRD struct{} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (NodeLabelSpecRD) ResourceDefinition(resource.Metadata, NodeLabelSpecSpec) meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: NodeLabelSpecType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + Sensitivity: meta.Sensitive, + PrintColumns: []meta.PrintColumn{ + { + Name: "Value", + JSONPath: "{.value}", + }, + }, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[NodeLabelSpecSpec](NodeLabelSpecType, &NodeLabelSpec{}) + if err != nil { + panic(err) + } +} diff --git a/website/content/v1.3/reference/api.md b/website/content/v1.3/reference/api.md index 6887f1097..3d24448f5 100644 --- a/website/content/v1.3/reference/api.md +++ b/website/content/v1.3/reference/api.md @@ -106,6 +106,7 @@ description: Talos gRPC API reference. - [ManifestStatusSpec](#talos.resource.definitions.k8s.ManifestStatusSpec) - [NodeIPConfigSpec](#talos.resource.definitions.k8s.NodeIPConfigSpec) - [NodeIPSpec](#talos.resource.definitions.k8s.NodeIPSpec) + - [NodeLabelSpecSpec](#talos.resource.definitions.k8s.NodeLabelSpecSpec) - [NodenameSpec](#talos.resource.definitions.k8s.NodenameSpec) - [SchedulerConfigSpec](#talos.resource.definitions.k8s.SchedulerConfigSpec) - [SchedulerConfigSpec.EnvironmentVariablesEntry](#talos.resource.definitions.k8s.SchedulerConfigSpec.EnvironmentVariablesEntry) @@ -1970,6 +1971,22 @@ NodeIPSpec holds the Node IP specification. + + +### NodeLabelSpecSpec +NodeLabelSpecSpec represents a label that's attached to a Talos node. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| key | [string](#string) | | | +| value | [string](#string) | | | + + + + + + ### NodenameSpec diff --git a/website/content/v1.3/reference/configuration.md b/website/content/v1.3/reference/configuration.md index 9391c4ec5..72ad8661f 100644 --- a/website/content/v1.3/reference/configuration.md +++ b/website/content/v1.3/reference/configuration.md @@ -400,6 +400,10 @@ seccompProfiles: value: defaultAction: SCMP_ACT_LOG {{< /highlight >}} | | +|`nodeLabels` |map[string]string |Configures the node labels for the machine.
Show example(s){{< highlight yaml >}} +nodeLabels: + exampleLabel: exampleLabelValue +{{< /highlight >}}
| | diff --git a/website/content/v1.3/talos-guides/configuration/editing-machine-configuration.md b/website/content/v1.3/talos-guides/configuration/editing-machine-configuration.md index c6314f6e3..8609c3cce 100644 --- a/website/content/v1.3/talos-guides/configuration/editing-machine-configuration.md +++ b/website/content/v1.3/talos-guides/configuration/editing-machine-configuration.md @@ -39,6 +39,7 @@ The list of config changes allowed to be applied immediately in Talos {{< releas * `.machine.certCANs` * `.machine.install` (configuration is only applied during install/upgrade) * `.machine.network` +* `.machine.nodeLabels` * `.machine.sysfs` * `.machine.sysctls` * `.machine.logging`