diff --git a/internal/app/machined/pkg/controllers/network/hostname_config.go b/internal/app/machined/pkg/controllers/network/hostname_config.go new file mode 100644 index 0000000000..51ea338fa5 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_config.go @@ -0,0 +1,229 @@ +// 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 network + +import ( + "context" + "fmt" + "strings" + + "github.com/AlekSi/pointer" + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/talos-systems/go-procfs/procfs" + "go.uber.org/zap" + + talosconfig "github.com/talos-systems/talos/pkg/machinery/config" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +// HostnameConfigController manages network.HostnameSpec based on machine configuration, kernel cmdline. +type HostnameConfigController struct { + Cmdline *procfs.Cmdline +} + +// Name implements controller.Controller interface. +func (ctrl *HostnameConfigController) Name() string { + return "network.HostnameConfigController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *HostnameConfigController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: pointer.ToString(config.V1Alpha1ID), + Kind: controller.InputWeak, + }, + { + Namespace: network.NamespaceName, + Type: network.NodeAddressType, + ID: pointer.ToString(network.NodeAddressDefaultID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *HostnameConfigController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.HostnameSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *HostnameConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + touchedIDs := make(map[resource.ID]struct{}) + + var cfgProvider talosconfig.Provider + + cfg, err := r.Get(ctx, 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 { + cfgProvider = cfg.(*config.MachineConfig).Config() + } + + var specs []network.HostnameSpecSpec + + // defaults + var defaultAddr *network.NodeAddress + + addrs, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.NodeAddressType, network.NodeAddressDefaultID, resource.VersionUndefined)) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error getting config: %w", err) + } + } else { + defaultAddr = addrs.(*network.NodeAddress) //nolint:errcheck,forcetypeassert + } + + specs = append(specs, ctrl.getDefault(defaultAddr)) + + // parse kernel cmdline for the default gateway + cmdlineHostname := ctrl.parseCmdline(logger) + if cmdlineHostname.Hostname != "" { + specs = append(specs, cmdlineHostname) + } + + // parse machine configuration for specs + if cfgProvider != nil { + configHostname := ctrl.parseMachineConfiguration(logger, cfgProvider) + + if configHostname.Hostname != "" { + specs = append(specs, configHostname) + } + } + + var ids []string + + ids, err = ctrl.apply(ctx, r, specs) + if err != nil { + return fmt.Errorf("error applying specs: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + + // list specs for cleanup + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + // skip specs created by other controllers + continue + } + + if _, ok := touchedIDs[res.Metadata().ID()]; !ok { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} + +//nolint:dupl +func (ctrl *HostnameConfigController) apply(ctx context.Context, r controller.Runtime, specs []network.HostnameSpecSpec) ([]resource.ID, error) { + ids := make([]string, 0, len(specs)) + + for _, spec := range specs { + spec := spec + id := network.LayeredID(spec.ConfigLayer, network.HostnameID) + + if err := r.Modify( + ctx, + network.NewHostnameSpec(network.ConfigNamespaceName, id), + func(r resource.Resource) error { + *r.(*network.HostnameSpec).TypedSpec() = spec + + return nil + }, + ); err != nil { + return ids, err + } + + ids = append(ids, id) + } + + return ids, nil +} + +func (ctrl *HostnameConfigController) getDefault(defaultAddr *network.NodeAddress) (spec network.HostnameSpecSpec) { + if defaultAddr == nil || len(defaultAddr.TypedSpec().Addresses) != 1 { + return + } + + spec.Hostname = fmt.Sprintf("talos-%s", strings.ReplaceAll(strings.ReplaceAll(defaultAddr.TypedSpec().Addresses[0].String(), ":", ""), ".", "-")) + spec.ConfigLayer = network.ConfigDefault + + return spec +} + +func (ctrl *HostnameConfigController) parseCmdline(logger *zap.Logger) (spec network.HostnameSpecSpec) { + if ctrl.Cmdline == nil { + return + } + + settings, err := ParseCmdlineNetwork(ctrl.Cmdline) + if err != nil { + logger.Warn("ignoring error", zap.Error(err)) + + return + } + + if settings.Hostname == "" { + return + } + + if err = spec.ParseFQDN(settings.Hostname); err != nil { + logger.Warn("ignoring error", zap.Error(err)) + + return network.HostnameSpecSpec{} + } + + spec.ConfigLayer = network.ConfigCmdline + + return spec +} + +func (ctrl *HostnameConfigController) parseMachineConfiguration(logger *zap.Logger, cfgProvider talosconfig.Provider) (spec network.HostnameSpecSpec) { + hostname := cfgProvider.Machine().Network().Hostname() + + if hostname == "" { + return + } + + if err := spec.ParseFQDN(hostname); err != nil { + logger.Warn("ignoring error", zap.Error(err)) + + return network.HostnameSpecSpec{} + } + + spec.ConfigLayer = network.ConfigMachineConfiguration + + return spec +} diff --git a/internal/app/machined/pkg/controllers/network/hostname_config_test.go b/internal/app/machined/pkg/controllers/network/hostname_config_test.go new file mode 100644 index 0000000000..306d5f0818 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_config_test.go @@ -0,0 +1,214 @@ +// 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 network_test + +import ( + "context" + "fmt" + "log" + "net/url" + "strings" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-procfs/procfs" + "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type HostnameConfigSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *HostnameConfigSuite) SetupTest() { + 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) +} + +func (suite *HostnameConfigSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *HostnameConfigSuite) assertHostnames(requiredIDs []string, check func(*network.HostnameSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.HostnameSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *HostnameConfigSuite) assertNoHostname(id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + if res.Metadata().ID() == id { + return retry.ExpectedError(fmt.Errorf("spec %q is still there", id)) + } + } + + return nil +} + +func (suite *HostnameConfigSuite) TestDefaults() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.HostnameConfigController{})) + + suite.startRuntime() + + defaultAddress := network.NewNodeAddress(network.NamespaceName, network.NodeAddressDefaultID) + defaultAddress.TypedSpec().Addresses = []netaddr.IP{netaddr.MustParseIP("33.11.22.44")} + + suite.Require().NoError(suite.state.Create(suite.ctx, defaultAddress)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertHostnames([]string{ + "default/hostname", + }, func(r *network.HostnameSpec) error { + suite.Assert().Equal("talos-33-11-22-44", r.TypedSpec().Hostname) + suite.Assert().Equal("", r.TypedSpec().Domainname) + suite.Assert().Equal(network.ConfigDefault, r.TypedSpec().ConfigLayer) + + return nil + }) + })) +} + +func (suite *HostnameConfigSuite) TestCmdline() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.HostnameConfigController{ + Cmdline: procfs.NewCmdline("ip=172.20.0.2:172.21.0.1:172.20.0.1:255.255.255.0:master1.domain.tld:eth1::10.0.0.1:10.0.0.2:10.0.0.1"), + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertHostnames([]string{ + "cmdline/hostname", + }, func(r *network.HostnameSpec) error { + suite.Assert().Equal("master1", r.TypedSpec().Hostname) + suite.Assert().Equal("domain.tld", r.TypedSpec().Domainname) + suite.Assert().Equal(network.ConfigCmdline, r.TypedSpec().ConfigLayer) + + return nil + }) + })) +} + +func (suite *HostnameConfigSuite) TestMachineConfiguration() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.HostnameConfigController{})) + + suite.startRuntime() + + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig(&v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineNetwork: &v1alpha1.NetworkConfig{ + NetworkHostname: "foo", + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: u, + }, + }, + }, + }) + + suite.Require().NoError(suite.state.Create(suite.ctx, cfg)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertHostnames([]string{ + "configuration/hostname", + }, func(r *network.HostnameSpec) error { + suite.Assert().Equal("foo", r.TypedSpec().Hostname) + suite.Assert().Equal("", r.TypedSpec().Domainname) + suite.Assert().Equal(network.ConfigMachineConfiguration, r.TypedSpec().ConfigLayer) + + return nil + }) + })) + + _, err = suite.state.UpdateWithConflicts(suite.ctx, cfg.Metadata(), func(r resource.Resource) error { + r.(*config.MachineConfig).Config().(*v1alpha1.Config).MachineConfig.MachineNetwork.NetworkHostname = strings.Repeat("a", 128) + + return nil + }) + suite.Require().NoError(err) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoHostname("configuration/hostname") + })) +} + +func TestHostnameConfigSuite(t *testing.T) { + suite.Run(t, new(HostnameConfigSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/hostname_merge.go b/internal/app/machined/pkg/controllers/network/hostname_merge.go new file mode 100644 index 0000000000..9588a1bb4f --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_merge.go @@ -0,0 +1,113 @@ +// 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 network provides controllers which manage network resources. +package network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// HostnameMergeController merges network.HostnameSpec in network.ConfigNamespace and produces final network.HostnameSpec in network.Namespace. +type HostnameMergeController struct{} + +// Name implements controller.Controller interface. +func (ctrl *HostnameMergeController) Name() string { + return "network.HostnameMergeController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *HostnameMergeController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.ConfigNamespaceName, + Type: network.HostnameSpecType, + Kind: controller.InputWeak, + }, + { + Namespace: network.NamespaceName, + Type: network.HostnameSpecType, + Kind: controller.InputDestroyReady, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *HostnameMergeController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.HostnameSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *HostnameMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // simply merge by layers, overriding with the next configuration layer + var final network.HostnameSpecSpec + + for _, res := range list.Items { + spec := res.(*network.HostnameSpec) //nolint:errcheck,forcetypeassert + + if final.Hostname != "" && spec.TypedSpec().ConfigLayer <= final.ConfigLayer { + // skip this spec, as existing one is higher layer + continue + } + + final = *spec.TypedSpec() + } + + if final.Hostname != "" { + if err = r.Modify(ctx, network.NewHostnameSpec(network.NamespaceName, network.HostnameID), func(res resource.Resource) error { + spec := res.(*network.HostnameSpec) //nolint:errcheck,forcetypeassert + + *spec.TypedSpec() = final + + return nil + }); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } + } else { + // remove existing + var okToDestroy bool + + md := resource.NewMetadata(network.NamespaceName, network.HostnameSpecType, network.HostnameID, resource.VersionUndefined) + + okToDestroy, err = r.Teardown(ctx, md) + if err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error cleaning up specs: %w", err) + } + + if okToDestroy { + if err = r.Destroy(ctx, md); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/hostname_merge_test.go b/internal/app/machined/pkg/controllers/network/hostname_merge_test.go new file mode 100644 index 0000000000..326c756b74 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_merge_test.go @@ -0,0 +1,158 @@ +// 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 network_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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type HostnameMergeSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *HostnameMergeSuite) SetupTest() { + 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(&netctrl.HostnameMergeController{})) + + suite.startRuntime() +} + +func (suite *HostnameMergeSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *HostnameMergeSuite) assertHostnames(requiredIDs []string, check func(*network.HostnameSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.HostnameSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *HostnameMergeSuite) TestMerge() { + def := network.NewHostnameSpec(network.ConfigNamespaceName, "default/hostname") + *def.TypedSpec() = network.HostnameSpecSpec{ + Hostname: "foo", + Domainname: "tld", + ConfigLayer: network.ConfigDefault, + } + + dhcp1 := network.NewHostnameSpec(network.ConfigNamespaceName, "dhcp/eth0") + *dhcp1.TypedSpec() = network.HostnameSpecSpec{ + Hostname: "eth-0", + ConfigLayer: network.ConfigDHCP, + } + + dhcp2 := network.NewHostnameSpec(network.ConfigNamespaceName, "dhcp/eth1") + *dhcp2.TypedSpec() = network.HostnameSpecSpec{ + Hostname: "eth-1", + ConfigLayer: network.ConfigDHCP, + } + + static := network.NewHostnameSpec(network.ConfigNamespaceName, "configuration/hostname") + *static.TypedSpec() = network.HostnameSpecSpec{ + Hostname: "bar", + Domainname: "com", + ConfigLayer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{def, dhcp1, dhcp2, static} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertHostnames([]string{ + "hostname", + }, func(r *network.HostnameSpec) error { + suite.Assert().Equal("bar.com", r.TypedSpec().FQDN()) + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, static.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertHostnames([]string{ + "hostname", + }, func(r *network.HostnameSpec) error { + if r.TypedSpec().FQDN() != "eth-0" { + return retry.ExpectedErrorf("unexpected hostname %q", r.TypedSpec().FQDN()) + } + + return nil + }) + })) +} + +func TestHostnameMergeSuite(t *testing.T) { + suite.Run(t, new(HostnameMergeSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/hostname_spec.go b/internal/app/machined/pkg/controllers/network/hostname_spec.go new file mode 100644 index 0000000000..6d206f73eb --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_spec.go @@ -0,0 +1,118 @@ +// 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 network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + "golang.org/x/sys/unix" + + v1alpha1runtime "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" + "github.com/talos-systems/talos/pkg/resources/network" +) + +// HostnameSpecController applies network.HostnameSpec to the actual interfaces. +type HostnameSpecController struct { + V1Alpha1Mode v1alpha1runtime.Mode +} + +// Name implements controller.Controller interface. +func (ctrl *HostnameSpecController) Name() string { + return "network.HostnameSpecController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *HostnameSpecController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.NamespaceName, + Type: network.HostnameSpecType, + Kind: controller.InputStrong, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *HostnameSpecController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.HostnameStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *HostnameSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // add finalizers for all live resources + for _, res := range list.Items { + if res.Metadata().Phase() != resource.PhaseRunning { + continue + } + + if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error adding finalizer: %w", err) + } + } + + // loop over specs and sync to statuses + for _, res := range list.Items { + spec := res.(*network.HostnameSpec) //nolint:forcetypeassert,errcheck + + switch spec.Metadata().Phase() { + case resource.PhaseTearingDown: + if err = r.Destroy(ctx, resource.NewMetadata(network.NamespaceName, network.HostnameStatusType, spec.Metadata().ID(), resource.VersionUndefined)); err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error destroying status: %w", err) + } + + if err = r.RemoveFinalizer(ctx, spec.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error removing finalizer: %w", err) + } + case resource.PhaseRunning: + if err = r.Modify(ctx, network.NewHostnameStatus(network.NamespaceName, spec.Metadata().ID()), func(r resource.Resource) error { + status := r.(*network.HostnameStatus) //nolint:forcetypeassert,errcheck + + status.TypedSpec().Hostname = spec.TypedSpec().Hostname + status.TypedSpec().Domainname = spec.TypedSpec().Domainname + + return nil + }); err != nil { + return fmt.Errorf("error modifying status: %w", err) + } + + // apply hostname unless running in container mode + if ctrl.V1Alpha1Mode != v1alpha1runtime.ModeContainer { + if err = unix.Sethostname([]byte(spec.TypedSpec().Hostname)); err != nil { + return fmt.Errorf("error setting hostname: %w", err) + } + + if err = unix.Setdomainname([]byte(spec.TypedSpec().Domainname)); err != nil { + return fmt.Errorf("error setting domainname: %w", err) + } + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/hostname_spec_test.go b/internal/app/machined/pkg/controllers/network/hostname_spec_test.go new file mode 100644 index 0000000000..0c17860257 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostname_spec_test.go @@ -0,0 +1,106 @@ +// 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 network_test + +import ( + "context" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + v1alpha1runtime "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type HostnameSpecSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *HostnameSpecSuite) SetupTest() { + 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(&netctrl.HostnameSpecController{ + V1Alpha1Mode: v1alpha1runtime.ModeContainer, // run in container mode to skip _actually_ setting hostname + })) + + suite.startRuntime() +} + +func (suite *HostnameSpecSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *HostnameSpecSuite) assertStatus(id string, fqdn string) error { + r, err := suite.state.Get(suite.ctx, resource.NewMetadata(network.NamespaceName, network.HostnameStatusType, id, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + return retry.ExpectedError(err) + } + + return err + } + + status := r.(*network.HostnameStatus) //nolint:errcheck,forcetypeassert + + if status.TypedSpec().FQDN() != fqdn { + return retry.ExpectedErrorf("fqdn mismatch: %q != %q", status.TypedSpec().FQDN(), fqdn) + } + + return nil +} + +func (suite *HostnameSpecSuite) TestSpec() { + spec := network.NewHostnameSpec(network.NamespaceName, "hostname") + *spec.TypedSpec() = network.HostnameSpecSpec{ + Hostname: "foo", + Domainname: "bar", + ConfigLayer: network.ConfigDefault, + } + + for _, res := range []resource.Resource{spec} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertStatus("hostname", "foo.bar") + })) +} + +func TestHostnameSpecSuite(t *testing.T) { + suite.Run(t, new(HostnameSpecSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_config.go b/internal/app/machined/pkg/controllers/network/resolver_config.go new file mode 100644 index 0000000000..a6860360ac --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_config.go @@ -0,0 +1,209 @@ +// 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 network + +import ( + "context" + "fmt" + + "github.com/AlekSi/pointer" + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/talos-systems/go-procfs/procfs" + "go.uber.org/zap" + "inet.af/netaddr" + + talosconfig "github.com/talos-systems/talos/pkg/machinery/config" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +// ResolverConfigController manages network.ResolverSpec based on machine configuration, kernel cmdline. +type ResolverConfigController struct { + Cmdline *procfs.Cmdline +} + +// Name implements controller.Controller interface. +func (ctrl *ResolverConfigController) Name() string { + return "network.ResolverConfigController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *ResolverConfigController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: pointer.ToString(config.V1Alpha1ID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *ResolverConfigController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.ResolverSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *ResolverConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + touchedIDs := make(map[resource.ID]struct{}) + + var cfgProvider talosconfig.Provider + + cfg, err := r.Get(ctx, 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 { + cfgProvider = cfg.(*config.MachineConfig).Config() + } + + var specs []network.ResolverSpecSpec + + // defaults + specs = append(specs, ctrl.getDefault()) + + // parse kernel cmdline for the default gateway + cmdlineServers := ctrl.parseCmdline(logger) + if cmdlineServers.DNSServers != nil { + specs = append(specs, cmdlineServers) + } + + // parse machine configuration for specs + if cfgProvider != nil { + configServers := ctrl.parseMachineConfiguration(logger, cfgProvider) + + if configServers.DNSServers != nil { + specs = append(specs, configServers) + } + } + + var ids []string + + ids, err = ctrl.apply(ctx, r, specs) + if err != nil { + return fmt.Errorf("error applying specs: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + + // list specs for cleanup + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + // skip specs created by other controllers + continue + } + + if _, ok := touchedIDs[res.Metadata().ID()]; !ok { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} + +//nolint:dupl +func (ctrl *ResolverConfigController) apply(ctx context.Context, r controller.Runtime, specs []network.ResolverSpecSpec) ([]resource.ID, error) { + ids := make([]string, 0, len(specs)) + + for _, spec := range specs { + spec := spec + id := network.LayeredID(spec.ConfigLayer, network.ResolverID) + + if err := r.Modify( + ctx, + network.NewResolverSpec(network.ConfigNamespaceName, id), + func(r resource.Resource) error { + *r.(*network.ResolverSpec).TypedSpec() = spec + + return nil + }, + ); err != nil { + return ids, err + } + + ids = append(ids, id) + } + + return ids, nil +} + +func (ctrl *ResolverConfigController) getDefault() (spec network.ResolverSpecSpec) { + spec.DNSServers = []netaddr.IP{netaddr.MustParseIP(constants.DefaultPrimaryResolver), netaddr.MustParseIP(constants.DefaultSecondaryResolver)} + spec.ConfigLayer = network.ConfigDefault + + return spec +} + +func (ctrl *ResolverConfigController) parseCmdline(logger *zap.Logger) (spec network.ResolverSpecSpec) { + if ctrl.Cmdline == nil { + return + } + + settings, err := ParseCmdlineNetwork(ctrl.Cmdline) + if err != nil { + logger.Warn("ignoring error", zap.Error(err)) + + return + } + + if len(settings.DNSAddresses) == 0 { + return + } + + spec.DNSServers = settings.DNSAddresses + spec.ConfigLayer = network.ConfigCmdline + + return spec +} + +func (ctrl *ResolverConfigController) parseMachineConfiguration(logger *zap.Logger, cfgProvider talosconfig.Provider) (spec network.ResolverSpecSpec) { + resolvers := cfgProvider.Machine().Network().Resolvers() + + if len(resolvers) == 0 { + return + } + + for i := range resolvers { + server, err := netaddr.ParseIP(resolvers[i]) + if err != nil { + logger.Warn("failed to parse DNS server", zap.String("server", resolvers[i]), zap.Error(err)) + + continue + } + + spec.DNSServers = append(spec.DNSServers, server) + } + + spec.ConfigLayer = network.ConfigMachineConfiguration + + return spec +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_config_test.go b/internal/app/machined/pkg/controllers/network/resolver_config_test.go new file mode 100644 index 0000000000..77a0f91933 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_config_test.go @@ -0,0 +1,204 @@ +// 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 network_test + +import ( + "context" + "fmt" + "log" + "net/url" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-procfs/procfs" + "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type ResolverConfigSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *ResolverConfigSuite) SetupTest() { + 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) +} + +func (suite *ResolverConfigSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *ResolverConfigSuite) assertResolvers(requiredIDs []string, check func(*network.ResolverSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.ResolverSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *ResolverConfigSuite) assertNoResolver(id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + if res.Metadata().ID() == id { + return retry.ExpectedError(fmt.Errorf("spec %q is still there", id)) + } + } + + return nil +} + +func (suite *ResolverConfigSuite) TestDefaults() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.ResolverConfigController{})) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResolvers([]string{ + "default/resolvers", + }, func(r *network.ResolverSpec) error { + suite.Assert().Equal([]netaddr.IP{netaddr.MustParseIP(constants.DefaultPrimaryResolver), netaddr.MustParseIP(constants.DefaultSecondaryResolver)}, r.TypedSpec().DNSServers) + suite.Assert().Equal(network.ConfigDefault, r.TypedSpec().ConfigLayer) + + return nil + }) + })) +} + +func (suite *ResolverConfigSuite) TestCmdline() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.ResolverConfigController{ + Cmdline: procfs.NewCmdline("ip=172.20.0.2:172.21.0.1:172.20.0.1:255.255.255.0:master1:eth1::10.0.0.1:10.0.0.2:10.0.0.1"), + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResolvers([]string{ + "cmdline/resolvers", + }, func(r *network.ResolverSpec) error { + suite.Assert().Equal([]netaddr.IP{netaddr.MustParseIP("10.0.0.1"), netaddr.MustParseIP("10.0.0.2")}, r.TypedSpec().DNSServers) + + return nil + }) + })) +} + +func (suite *ResolverConfigSuite) TestMachineConfiguration() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.ResolverConfigController{})) + + suite.startRuntime() + + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig(&v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineNetwork: &v1alpha1.NetworkConfig{ + NameServers: []string{"2.2.2.2", "3.3.3.3"}, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: u, + }, + }, + }, + }) + + suite.Require().NoError(suite.state.Create(suite.ctx, cfg)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResolvers([]string{ + "configuration/resolvers", + }, func(r *network.ResolverSpec) error { + suite.Assert().Equal([]netaddr.IP{netaddr.MustParseIP("2.2.2.2"), netaddr.MustParseIP("3.3.3.3")}, r.TypedSpec().DNSServers) + + return nil + }) + })) + + _, err = suite.state.UpdateWithConflicts(suite.ctx, cfg.Metadata(), func(r resource.Resource) error { + r.(*config.MachineConfig).Config().(*v1alpha1.Config).MachineConfig.MachineNetwork.NameServers = nil + + return nil + }) + suite.Require().NoError(err) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoResolver("configuration/resolvers") + })) +} + +func TestResolverConfigSuite(t *testing.T) { + suite.Run(t, new(ResolverConfigSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_merge.go b/internal/app/machined/pkg/controllers/network/resolver_merge.go new file mode 100644 index 0000000000..09e6a50b3d --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_merge.go @@ -0,0 +1,119 @@ +// 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 network provides controllers which manage network resources. +package network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// ResolverMergeController merges network.ResolverSpec in network.ConfigNamespace and produces final network.ResolverSpec in network.Namespace. +type ResolverMergeController struct{} + +// Name implements controller.Controller interface. +func (ctrl *ResolverMergeController) Name() string { + return "network.ResolverMergeController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *ResolverMergeController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.ConfigNamespaceName, + Type: network.ResolverSpecType, + Kind: controller.InputWeak, + }, + { + Namespace: network.NamespaceName, + Type: network.ResolverSpecType, + Kind: controller.InputDestroyReady, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *ResolverMergeController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.ResolverSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *ResolverMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // simply merge by layers, overriding with the next configuration layer + var final network.ResolverSpecSpec + + for _, res := range list.Items { + spec := res.(*network.ResolverSpec) //nolint:errcheck,forcetypeassert + + if final.DNSServers != nil && spec.TypedSpec().ConfigLayer < final.ConfigLayer { + // skip this spec, as existing one is higher layer + continue + } + + if spec.TypedSpec().ConfigLayer == final.ConfigLayer { + // merge server lists on the same level + final.DNSServers = append(final.DNSServers, spec.TypedSpec().DNSServers...) + } else { + // otherwise, replace the lists + final = *spec.TypedSpec() + } + } + + if final.DNSServers != nil { + if err = r.Modify(ctx, network.NewResolverSpec(network.NamespaceName, network.ResolverID), func(res resource.Resource) error { + spec := res.(*network.ResolverSpec) //nolint:errcheck,forcetypeassert + + *spec.TypedSpec() = final + + return nil + }); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } + } else { + // remove existing + var okToDestroy bool + + md := resource.NewMetadata(network.NamespaceName, network.ResolverSpecType, network.ResolverID, resource.VersionUndefined) + + okToDestroy, err = r.Teardown(ctx, md) + if err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error cleaning up specs: %w", err) + } + + if okToDestroy { + if err = r.Destroy(ctx, md); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_merge_test.go b/internal/app/machined/pkg/controllers/network/resolver_merge_test.go new file mode 100644 index 0000000000..ce64a8035c --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_merge_test.go @@ -0,0 +1,159 @@ +// 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 network_test + +import ( + "context" + "fmt" + "log" + "reflect" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type ResolverMergeSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *ResolverMergeSuite) SetupTest() { + 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(&netctrl.ResolverMergeController{})) + + suite.startRuntime() +} + +func (suite *ResolverMergeSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *ResolverMergeSuite) assertResolvers(requiredIDs []string, check func(*network.ResolverSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.ResolverSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *ResolverMergeSuite) TestMerge() { + def := network.NewResolverSpec(network.ConfigNamespaceName, "default/resolvers") + *def.TypedSpec() = network.ResolverSpecSpec{ + DNSServers: []netaddr.IP{netaddr.MustParseIP(constants.DefaultPrimaryResolver), netaddr.MustParseIP(constants.DefaultSecondaryResolver)}, + ConfigLayer: network.ConfigDefault, + } + + dhcp1 := network.NewResolverSpec(network.ConfigNamespaceName, "dhcp/eth0") + *dhcp1.TypedSpec() = network.ResolverSpecSpec{ + DNSServers: []netaddr.IP{netaddr.MustParseIP("1.1.2.0")}, + ConfigLayer: network.ConfigDHCP, + } + + dhcp2 := network.NewResolverSpec(network.ConfigNamespaceName, "dhcp/eth1") + *dhcp2.TypedSpec() = network.ResolverSpecSpec{ + DNSServers: []netaddr.IP{netaddr.MustParseIP("1.1.2.1")}, + ConfigLayer: network.ConfigDHCP, + } + + static := network.NewResolverSpec(network.ConfigNamespaceName, "configuration/resolvers") + *static.TypedSpec() = network.ResolverSpecSpec{ + DNSServers: []netaddr.IP{netaddr.MustParseIP("2.2.2.2")}, + ConfigLayer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{def, dhcp1, dhcp2, static} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResolvers([]string{ + "resolvers", + }, func(r *network.ResolverSpec) error { + suite.Assert().Equal(*static.TypedSpec(), *r.TypedSpec()) + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, static.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResolvers([]string{ + "resolvers", + }, func(r *network.ResolverSpec) error { + if !reflect.DeepEqual(r.TypedSpec().DNSServers, []netaddr.IP{netaddr.MustParseIP("1.1.2.0"), netaddr.MustParseIP("1.1.2.1")}) { + return retry.ExpectedErrorf("unexpected servers %q", r.TypedSpec().DNSServers) + } + + return nil + }) + })) +} + +func TestResolverMergeSuite(t *testing.T) { + suite.Run(t, new(ResolverMergeSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_spec.go b/internal/app/machined/pkg/controllers/network/resolver_spec.go new file mode 100644 index 0000000000..c7183b164e --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_spec.go @@ -0,0 +1,102 @@ +// 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 network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// ResolverSpecController applies network.ResolverSpec to the actual interfaces. +type ResolverSpecController struct{} + +// Name implements controller.Controller interface. +func (ctrl *ResolverSpecController) Name() string { + return "network.ResolverSpecController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *ResolverSpecController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.NamespaceName, + Type: network.ResolverSpecType, + Kind: controller.InputStrong, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *ResolverSpecController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.ResolverStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *ResolverSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.ResolverSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // add finalizers for all live resources + for _, res := range list.Items { + if res.Metadata().Phase() != resource.PhaseRunning { + continue + } + + if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error adding finalizer: %w", err) + } + } + + // loop over specs and sync to statuses + for _, res := range list.Items { + spec := res.(*network.ResolverSpec) //nolint:forcetypeassert,errcheck + + switch spec.Metadata().Phase() { + case resource.PhaseTearingDown: + if err = r.Destroy(ctx, resource.NewMetadata(network.NamespaceName, network.ResolverStatusType, spec.Metadata().ID(), resource.VersionUndefined)); err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error destroying status: %w", err) + } + + if err = r.RemoveFinalizer(ctx, spec.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error removing finalizer: %w", err) + } + case resource.PhaseRunning: + if err = r.Modify(ctx, network.NewResolverStatus(network.NamespaceName, spec.Metadata().ID()), func(r resource.Resource) error { + status := r.(*network.ResolverStatus) //nolint:forcetypeassert,errcheck + + status.TypedSpec().DNSServers = spec.TypedSpec().DNSServers + + return nil + }); err != nil { + return fmt.Errorf("error modifying status: %w", err) + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/resolver_spec_test.go b/internal/app/machined/pkg/controllers/network/resolver_spec_test.go new file mode 100644 index 0000000000..899d2a3e34 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/resolver_spec_test.go @@ -0,0 +1,106 @@ +// 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 network_test + +import ( + "context" + "log" + "reflect" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type ResolverSpecSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *ResolverSpecSuite) SetupTest() { + 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(&netctrl.ResolverSpecController{})) + + suite.startRuntime() +} + +func (suite *ResolverSpecSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *ResolverSpecSuite) assertStatus(id string, servers ...netaddr.IP) error { + r, err := suite.state.Get(suite.ctx, resource.NewMetadata(network.NamespaceName, network.ResolverStatusType, id, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + return retry.ExpectedError(err) + } + + return err + } + + status := r.(*network.ResolverStatus) //nolint:errcheck,forcetypeassert + + if !reflect.DeepEqual(status.TypedSpec().DNSServers, servers) { + return retry.ExpectedErrorf("server list mismatch: %q != %q", status.TypedSpec().DNSServers, servers) + } + + return nil +} + +func (suite *ResolverSpecSuite) TestSpec() { + spec := network.NewResolverSpec(network.NamespaceName, "resolvers") + *spec.TypedSpec() = network.ResolverSpecSpec{ + DNSServers: []netaddr.IP{netaddr.MustParseIP(constants.DefaultPrimaryResolver)}, + ConfigLayer: network.ConfigDefault, + } + + for _, res := range []resource.Resource{spec} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertStatus("resolvers", netaddr.MustParseIP(constants.DefaultPrimaryResolver)) + })) +} + +func TestResolverSpecSuite(t *testing.T) { + suite.Run(t, new(ResolverSpecSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_config.go b/internal/app/machined/pkg/controllers/network/timeserver_config.go new file mode 100644 index 0000000000..299556a421 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_config.go @@ -0,0 +1,200 @@ +// 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 network + +import ( + "context" + "fmt" + + "github.com/AlekSi/pointer" + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "github.com/talos-systems/go-procfs/procfs" + "go.uber.org/zap" + + talosconfig "github.com/talos-systems/talos/pkg/machinery/config" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +// TimeServerConfigController manages network.TimeServerSpec based on machine configuration, kernel cmdline. +type TimeServerConfigController struct { + Cmdline *procfs.Cmdline +} + +// Name implements controller.Controller interface. +func (ctrl *TimeServerConfigController) Name() string { + return "network.TimeServerConfigController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *TimeServerConfigController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: pointer.ToString(config.V1Alpha1ID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *TimeServerConfigController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.TimeServerSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *TimeServerConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + touchedIDs := make(map[resource.ID]struct{}) + + var cfgProvider talosconfig.Provider + + cfg, err := r.Get(ctx, 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 { + cfgProvider = cfg.(*config.MachineConfig).Config() + } + + var specs []network.TimeServerSpecSpec + + // defaults + specs = append(specs, ctrl.getDefault()) + + // parse kernel cmdline for the default gateway + cmdlineServers := ctrl.parseCmdline(logger) + if cmdlineServers.NTPServers != nil { + specs = append(specs, cmdlineServers) + } + + // parse machine configuration for specs + if cfgProvider != nil { + configServers := ctrl.parseMachineConfiguration(cfgProvider) + + if configServers.NTPServers != nil { + specs = append(specs, configServers) + } + } + + var ids []string + + ids, err = ctrl.apply(ctx, r, specs) + if err != nil { + return fmt.Errorf("error applying specs: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + + // list specs for cleanup + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + // skip specs created by other controllers + continue + } + + if _, ok := touchedIDs[res.Metadata().ID()]; !ok { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} + +//nolint:dupl +func (ctrl *TimeServerConfigController) apply(ctx context.Context, r controller.Runtime, specs []network.TimeServerSpecSpec) ([]resource.ID, error) { + ids := make([]string, 0, len(specs)) + + for _, spec := range specs { + spec := spec + id := network.LayeredID(spec.ConfigLayer, network.TimeServerID) + + if err := r.Modify( + ctx, + network.NewTimeServerSpec(network.ConfigNamespaceName, id), + func(r resource.Resource) error { + *r.(*network.TimeServerSpec).TypedSpec() = spec + + return nil + }, + ); err != nil { + return ids, err + } + + ids = append(ids, id) + } + + return ids, nil +} + +func (ctrl *TimeServerConfigController) getDefault() (spec network.TimeServerSpecSpec) { + spec.NTPServers = []string{constants.DefaultNTPServer} + spec.ConfigLayer = network.ConfigDefault + + return spec +} + +func (ctrl *TimeServerConfigController) parseCmdline(logger *zap.Logger) (spec network.TimeServerSpecSpec) { + if ctrl.Cmdline == nil { + return + } + + settings, err := ParseCmdlineNetwork(ctrl.Cmdline) + if err != nil { + logger.Warn("ignoring error", zap.Error(err)) + + return + } + + if len(settings.NTPAddresses) == 0 { + return + } + + spec.NTPServers = make([]string, len(settings.NTPAddresses)) + spec.ConfigLayer = network.ConfigCmdline + + for i := range settings.NTPAddresses { + spec.NTPServers[i] = settings.NTPAddresses[i].String() + } + + return spec +} + +func (ctrl *TimeServerConfigController) parseMachineConfiguration(cfgProvider talosconfig.Provider) (spec network.TimeServerSpecSpec) { + if len(cfgProvider.Machine().Time().Servers()) == 0 { + return + } + + spec.NTPServers = append([]string(nil), cfgProvider.Machine().Time().Servers()...) + spec.ConfigLayer = network.ConfigMachineConfiguration + + return spec +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_config_test.go b/internal/app/machined/pkg/controllers/network/timeserver_config_test.go new file mode 100644 index 0000000000..2e281f3c57 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_config_test.go @@ -0,0 +1,203 @@ +// 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 network_test + +import ( + "context" + "fmt" + "log" + "net/url" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-procfs/procfs" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type TimeServerConfigSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *TimeServerConfigSuite) SetupTest() { + 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) +} + +func (suite *TimeServerConfigSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *TimeServerConfigSuite) assertTimeServers(requiredIDs []string, check func(*network.TimeServerSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.TimeServerSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *TimeServerConfigSuite) assertNoTimeServer(id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + if res.Metadata().ID() == id { + return retry.ExpectedError(fmt.Errorf("spec %q is still there", id)) + } + } + + return nil +} + +func (suite *TimeServerConfigSuite) TestDefaults() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.TimeServerConfigController{})) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertTimeServers([]string{ + "default/timeservers", + }, func(r *network.TimeServerSpec) error { + suite.Assert().Equal([]string{constants.DefaultNTPServer}, r.TypedSpec().NTPServers) + suite.Assert().Equal(network.ConfigDefault, r.TypedSpec().ConfigLayer) + + return nil + }) + })) +} + +func (suite *TimeServerConfigSuite) TestCmdline() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.TimeServerConfigController{ + Cmdline: procfs.NewCmdline("ip=172.20.0.2:172.21.0.1:172.20.0.1:255.255.255.0:master1:eth1::10.0.0.1:10.0.0.2:10.0.0.1"), + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertTimeServers([]string{ + "cmdline/timeservers", + }, func(r *network.TimeServerSpec) error { + suite.Assert().Equal([]string{"10.0.0.1"}, r.TypedSpec().NTPServers) + + return nil + }) + })) +} + +func (suite *TimeServerConfigSuite) TestMachineConfiguration() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.TimeServerConfigController{})) + + suite.startRuntime() + + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig(&v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineTime: &v1alpha1.TimeConfig{ + TimeServers: []string{"za.pool.ntp.org", "pool.ntp.org"}, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: u, + }, + }, + }, + }) + + suite.Require().NoError(suite.state.Create(suite.ctx, cfg)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertTimeServers([]string{ + "configuration/timeservers", + }, func(r *network.TimeServerSpec) error { + suite.Assert().Equal([]string{"za.pool.ntp.org", "pool.ntp.org"}, r.TypedSpec().NTPServers) + + return nil + }) + })) + + _, err = suite.state.UpdateWithConflicts(suite.ctx, cfg.Metadata(), func(r resource.Resource) error { + r.(*config.MachineConfig).Config().(*v1alpha1.Config).MachineConfig.MachineTime = nil + + return nil + }) + suite.Require().NoError(err) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoTimeServer("configuration/timeservers") + })) +} + +func TestTimeServerConfigSuite(t *testing.T) { + suite.Run(t, new(TimeServerConfigSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_merge.go b/internal/app/machined/pkg/controllers/network/timeserver_merge.go new file mode 100644 index 0000000000..065ed1172d --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_merge.go @@ -0,0 +1,119 @@ +// 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 network provides controllers which manage network resources. +package network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// TimeServerMergeController merges network.TimeServerSpec in network.ConfigNamespace and produces final network.TimeServerSpec in network.Namespace. +type TimeServerMergeController struct{} + +// Name implements controller.Controller interface. +func (ctrl *TimeServerMergeController) Name() string { + return "network.TimeServerMergeController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *TimeServerMergeController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.ConfigNamespaceName, + Type: network.TimeServerSpecType, + Kind: controller.InputWeak, + }, + { + Namespace: network.NamespaceName, + Type: network.TimeServerSpecType, + Kind: controller.InputDestroyReady, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *TimeServerMergeController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.TimeServerSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *TimeServerMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // simply merge by layers, overriding with the next configuration layer + var final network.TimeServerSpecSpec + + for _, res := range list.Items { + spec := res.(*network.TimeServerSpec) //nolint:errcheck,forcetypeassert + + if final.NTPServers != nil && spec.TypedSpec().ConfigLayer < final.ConfigLayer { + // skip this spec, as existing one is higher layer + continue + } + + if spec.TypedSpec().ConfigLayer == final.ConfigLayer { + // merge server lists on the same level + final.NTPServers = append(final.NTPServers, spec.TypedSpec().NTPServers...) + } else { + // otherwise, replace the lists + final = *spec.TypedSpec() + } + } + + if final.NTPServers != nil { + if err = r.Modify(ctx, network.NewTimeServerSpec(network.NamespaceName, network.TimeServerID), func(res resource.Resource) error { + spec := res.(*network.TimeServerSpec) //nolint:errcheck,forcetypeassert + + *spec.TypedSpec() = final + + return nil + }); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } + } else { + // remove existing + var okToDestroy bool + + md := resource.NewMetadata(network.NamespaceName, network.TimeServerSpecType, network.TimeServerID, resource.VersionUndefined) + + okToDestroy, err = r.Teardown(ctx, md) + if err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error cleaning up specs: %w", err) + } + + if okToDestroy { + if err = r.Destroy(ctx, md); err != nil { + return fmt.Errorf("error cleaning up specs: %w", err) + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_merge_test.go b/internal/app/machined/pkg/controllers/network/timeserver_merge_test.go new file mode 100644 index 0000000000..f0baf60ec3 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_merge_test.go @@ -0,0 +1,158 @@ +// 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 network_test + +import ( + "context" + "fmt" + "log" + "reflect" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type TimeServerMergeSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *TimeServerMergeSuite) SetupTest() { + 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(&netctrl.TimeServerMergeController{})) + + suite.startRuntime() +} + +func (suite *TimeServerMergeSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *TimeServerMergeSuite) assertTimeServers(requiredIDs []string, check func(*network.TimeServerSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.TimeServerSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *TimeServerMergeSuite) TestMerge() { + def := network.NewTimeServerSpec(network.ConfigNamespaceName, "default/timeservers") + *def.TypedSpec() = network.TimeServerSpecSpec{ + NTPServers: []string{constants.DefaultNTPServer}, + ConfigLayer: network.ConfigDefault, + } + + dhcp1 := network.NewTimeServerSpec(network.ConfigNamespaceName, "dhcp/eth0") + *dhcp1.TypedSpec() = network.TimeServerSpecSpec{ + NTPServers: []string{"ntp.eth0"}, + ConfigLayer: network.ConfigDHCP, + } + + dhcp2 := network.NewTimeServerSpec(network.ConfigNamespaceName, "dhcp/eth1") + *dhcp2.TypedSpec() = network.TimeServerSpecSpec{ + NTPServers: []string{"ntp.eth1"}, + ConfigLayer: network.ConfigDHCP, + } + + static := network.NewTimeServerSpec(network.ConfigNamespaceName, "configuration/timeservers") + *static.TypedSpec() = network.TimeServerSpecSpec{ + NTPServers: []string{"my.ntp"}, + ConfigLayer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{def, dhcp1, dhcp2, static} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertTimeServers([]string{ + "timeservers", + }, func(r *network.TimeServerSpec) error { + suite.Assert().Equal(*static.TypedSpec(), *r.TypedSpec()) + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, static.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertTimeServers([]string{ + "timeservers", + }, func(r *network.TimeServerSpec) error { + if !reflect.DeepEqual(r.TypedSpec().NTPServers, []string{"ntp.eth0", "ntp.eth1"}) { + return retry.ExpectedErrorf("unexpected servers %q", r.TypedSpec().NTPServers) + } + + return nil + }) + })) +} + +func TestTimeServerMergeSuite(t *testing.T) { + suite.Run(t, new(TimeServerMergeSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_spec.go b/internal/app/machined/pkg/controllers/network/timeserver_spec.go new file mode 100644 index 0000000000..23d4b03699 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_spec.go @@ -0,0 +1,104 @@ +// 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 network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/resources/network" +) + +// TimeServerSpecController applies network.TimeServerSpec to the actual interfaces. +type TimeServerSpecController struct{} + +// Name implements controller.Controller interface. +func (ctrl *TimeServerSpecController) Name() string { + return "network.TimeServerSpecController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *TimeServerSpecController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.NamespaceName, + Type: network.TimeServerSpecType, + Kind: controller.InputStrong, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *TimeServerSpecController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.TimeServerStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *TimeServerSpecController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // as there's nothing to do actually apply time servers, simply copy spec to status + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.TimeServerSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network addresses: %w", err) + } + + // add finalizers for all live resources + for _, res := range list.Items { + if res.Metadata().Phase() != resource.PhaseRunning { + continue + } + + if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error adding finalizer: %w", err) + } + } + + // loop over specs and sync to statuses + for _, res := range list.Items { + spec := res.(*network.TimeServerSpec) //nolint:forcetypeassert,errcheck + + switch spec.Metadata().Phase() { + case resource.PhaseTearingDown: + if err = r.Destroy(ctx, resource.NewMetadata(network.NamespaceName, network.TimeServerStatusType, spec.Metadata().ID(), resource.VersionUndefined)); err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("error destroying status: %w", err) + } + + if err = r.RemoveFinalizer(ctx, spec.Metadata(), ctrl.Name()); err != nil { + return fmt.Errorf("error removing finalizer: %w", err) + } + case resource.PhaseRunning: + if err = r.Modify(ctx, network.NewTimeServerStatus(network.NamespaceName, spec.Metadata().ID()), func(r resource.Resource) error { + status := r.(*network.TimeServerStatus) //nolint:forcetypeassert,errcheck + + status.TypedSpec().NTPServers = spec.TypedSpec().NTPServers + + return nil + }); err != nil { + return fmt.Errorf("error modifying status: %w", err) + } + } + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/timeserver_spec_test.go b/internal/app/machined/pkg/controllers/network/timeserver_spec_test.go new file mode 100644 index 0000000000..cad6c326eb --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/timeserver_spec_test.go @@ -0,0 +1,105 @@ +// 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 network_test + +import ( + "context" + "log" + "reflect" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/resources/network" +) + +type TimeServerSpecSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *TimeServerSpecSuite) SetupTest() { + 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(&netctrl.TimeServerSpecController{})) + + suite.startRuntime() +} + +func (suite *TimeServerSpecSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *TimeServerSpecSuite) assertStatus(id string, servers ...string) error { + r, err := suite.state.Get(suite.ctx, resource.NewMetadata(network.NamespaceName, network.TimeServerStatusType, id, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + return retry.ExpectedError(err) + } + + return err + } + + status := r.(*network.TimeServerStatus) //nolint:errcheck,forcetypeassert + + if !reflect.DeepEqual(status.TypedSpec().NTPServers, servers) { + return retry.ExpectedErrorf("server list mismatch: %q != %q", status.TypedSpec().NTPServers, servers) + } + + return nil +} + +func (suite *TimeServerSpecSuite) TestSpec() { + spec := network.NewTimeServerSpec(network.NamespaceName, "timeservers") + *spec.TypedSpec() = network.TimeServerSpecSpec{ + NTPServers: []string{constants.DefaultNTPServer}, + ConfigLayer: network.ConfigDefault, + } + + for _, res := range []resource.Resource{spec} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertStatus("timeservers", constants.DefaultNTPServer) + })) +} + +func TestTimeServerSpecSuite(t *testing.T) { + suite.Run(t, new(TimeServerSpecSuite)) +} diff --git a/internal/app/machined/pkg/controllers/time/sync.go b/internal/app/machined/pkg/controllers/time/sync.go index 9ccbb7d0dc..90220363c5 100644 --- a/internal/app/machined/pkg/controllers/time/sync.go +++ b/internal/app/machined/pkg/controllers/time/sync.go @@ -17,8 +17,8 @@ import ( v1alpha1runtime "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/pkg/ntp" - "github.com/talos-systems/talos/pkg/machinery/constants" "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" "github.com/talos-systems/talos/pkg/resources/time" ) @@ -36,11 +36,16 @@ func (ctrl *SyncController) Name() string { // Inputs implements controller.Controller interface. func (ctrl *SyncController) Inputs() []controller.Input { return []controller.Input{ + { + Namespace: network.NamespaceName, + Type: network.TimeServerStatusType, + ID: pointer.ToString(network.TimeServerID), + Kind: controller.InputWeak, + }, { Namespace: config.NamespaceName, Type: config.MachineConfigType, ID: pointer.ToString(config.V1Alpha1ID), - Kind: controller.InputWeak, }, } } @@ -109,6 +114,18 @@ func (ctrl *SyncController) Run(ctx context.Context, r controller.Runtime, logge epoch++ } + timeServersStatus, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.TimeServerStatusType, network.TimeServerID, resource.VersionUndefined)) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error getting time server status: %w", err) + } + + // time server list is not ready yet, wait for the next reconcile + continue + } + + timeServers := timeServersStatus.(*network.TimeServerStatus).TypedSpec().NTPServers + cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined)) if err != nil { if !state.IsNotFoundError(err) { @@ -126,11 +143,6 @@ func (ctrl *SyncController) Run(ctx context.Context, r controller.Runtime, logge syncDisabled = true } - timeServers := []string{constants.DefaultNTPServer} - if cfg != nil { - timeServers = cfg.(*config.MachineConfig).Config().Machine().Time().Servers() - } - switch { case syncDisabled && syncer != nil: // stop syncing diff --git a/internal/app/machined/pkg/controllers/time/sync_test.go b/internal/app/machined/pkg/controllers/time/sync_test.go index 0cf82dd126..d89892177f 100644 --- a/internal/app/machined/pkg/controllers/time/sync_test.go +++ b/internal/app/machined/pkg/controllers/time/sync_test.go @@ -28,6 +28,7 @@ import ( "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" "github.com/talos-systems/talos/pkg/machinery/constants" "github.com/talos-systems/talos/pkg/resources/config" + "github.com/talos-systems/talos/pkg/resources/network" timeresource "github.com/talos-systems/talos/pkg/resources/time" v1alpha1resource "github.com/talos-systems/talos/pkg/resources/v1alpha1" ) @@ -95,6 +96,10 @@ func (suite *SyncSuite) TestReconcileContainerMode() { NewNTPSyncer: suite.newMockSyncer, })) + timeServers := network.NewTimeServerStatus(network.NamespaceName, network.TimeServerID) + timeServers.TypedSpec().NTPServers = []string{constants.DefaultNTPServer} + suite.Require().NoError(suite.state.Create(suite.ctx, timeServers)) + suite.startRuntime() suite.Assert().NoError(retry.Constant(10*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( @@ -118,6 +123,10 @@ func (suite *SyncSuite) TestReconcileSyncDisabled() { suite.startRuntime() + timeServers := network.NewTimeServerStatus(network.NamespaceName, network.TimeServerID) + timeServers.TypedSpec().NTPServers = []string{constants.DefaultNTPServer} + suite.Require().NoError(suite.state.Create(suite.ctx, timeServers)) + suite.Assert().NoError(retry.Constant(10*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertTimeStatus( @@ -163,6 +172,10 @@ func (suite *SyncSuite) TestReconcileSyncDefaultConfig() { suite.startRuntime() + timeServers := network.NewTimeServerStatus(network.NamespaceName, network.TimeServerID) + timeServers.TypedSpec().NTPServers = []string{constants.DefaultNTPServer} + suite.Require().NoError(suite.state.Create(suite.ctx, timeServers)) + cfg := config.NewMachineConfig(&v1alpha1.Config{ ConfigVersion: "v1alpha1", MachineConfig: &v1alpha1.MachineConfig{}, @@ -192,6 +205,10 @@ func (suite *SyncSuite) TestReconcileSyncChangeConfig() { suite.startRuntime() + timeServers := network.NewTimeServerStatus(network.NamespaceName, network.TimeServerID) + timeServers.TypedSpec().NTPServers = []string{constants.DefaultNTPServer} + suite.Require().NoError(suite.state.Create(suite.ctx, timeServers)) + suite.Assert().NoError(retry.Constant(10*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertTimeStatus( @@ -254,10 +271,8 @@ func (suite *SyncSuite) TestReconcileSyncChangeConfig() { }, )) - _, err := suite.state.UpdateWithConflicts(suite.ctx, cfg.Metadata(), func(r resource.Resource) error { - r.(*config.MachineConfig).Config().(*v1alpha1.Config).MachineConfig.MachineTime = &v1alpha1.TimeConfig{ - TimeServers: []string{"127.0.0.1"}, - } + _, err := suite.state.UpdateWithConflicts(suite.ctx, timeServers.Metadata(), func(r resource.Resource) error { + r.(*network.TimeServerStatus).TypedSpec().NTPServers = []string{"127.0.0.1"} return nil }) diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index b74771d962..d2ddb8217b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -76,6 +76,14 @@ func (ctrl *Controller) Run(ctx context.Context) error { &network.AddressMergeController{}, &network.AddressSpecController{}, &network.AddressStatusController{}, + &network.HostnameConfigController{ + Cmdline: procfs.ProcCmdline(), + }, + &network.HostnameMergeController{}, + // TODO: disabled to avoid conflict with networkd + // &network.HostnameSpecController{ + // V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), + // }, &network.LinkConfigController{ Cmdline: procfs.ProcCmdline(), }, @@ -83,13 +91,23 @@ func (ctrl *Controller) Run(ctx context.Context) error { &network.LinkStatusController{}, &network.LinkSpecController{}, &network.NodeAddressController{}, + &network.ResolverConfigController{ + Cmdline: procfs.ProcCmdline(), + }, + &network.ResolverMergeController{}, + &network.ResolverSpecController{}, &network.RouteConfigController{ Cmdline: procfs.ProcCmdline(), }, &network.RouteMergeController{}, &network.RouteStatusController{}, &network.RouteSpecController{}, + &network.TimeServerConfigController{ + Cmdline: procfs.ProcCmdline(), + }, + &network.TimeServerMergeController{}, &perf.StatsController{}, + &network.TimeServerSpecController{}, &secrets.EtcdController{}, &secrets.KubernetesController{}, &secrets.RootController{}, diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index d86a0cb81a..4fe98625b5 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -81,12 +81,18 @@ func NewState() (*State, error) { &k8s.SecretsStatus{}, &network.AddressStatus{}, &network.AddressSpec{}, + &network.HostnameStatus{}, + &network.HostnameSpec{}, &network.LinkRefresh{}, &network.LinkStatus{}, &network.LinkSpec{}, &network.NodeAddress{}, + &network.ResolverStatus{}, + &network.ResolverSpec{}, &network.RouteStatus{}, &network.RouteSpec{}, + &network.TimeServerStatus{}, + &network.TimeServerSpec{}, &perf.CPU{}, &perf.Memory{}, &secrets.Etcd{}, diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go index 15f3631546..d55b964a4a 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go @@ -824,10 +824,6 @@ func (t *TimeConfig) Disabled() bool { // Servers implements the config.Provider interface. func (t *TimeConfig) Servers() []string { - if len(t.TimeServers) == 0 { - return []string{constants.DefaultNTPServer} - } - return t.TimeServers } diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 1c44d05ab7..3495882a5c 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -415,6 +415,12 @@ const ( // https://manage.ntppool.org/manage/vendor DefaultNTPServer = "pool.ntp.org" + // DefaultPrimaryResolver is the default primary DNS server. + DefaultPrimaryResolver = "1.1.1.1" + + // DefaultSecondaryResolver is the default secondary DNS server. + DefaultSecondaryResolver = "8.8.8.8" + // RoleAdmin defines Talos role for admins. // It matches Organization value of Talos client certificate. RoleAdmin = "os:admin" diff --git a/pkg/resources/network/address_spec.go b/pkg/resources/network/address_spec.go index 2f56019d69..26b30a983a 100644 --- a/pkg/resources/network/address_spec.go +++ b/pkg/resources/network/address_spec.go @@ -33,7 +33,7 @@ type AddressSpecSpec struct { ConfigLayer ConfigLayer `yaml:"layer"` } -// NewAddressSpec initializes a SecretsStatus resource. +// NewAddressSpec initializes a AddressSpec resource. func NewAddressSpec(namespace resource.Namespace, id resource.ID) *AddressSpec { r := &AddressSpec{ md: resource.NewMetadata(namespace, AddressSpecType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/address_status.go b/pkg/resources/network/address_status.go index 6aa46286d8..35e88f8728 100644 --- a/pkg/resources/network/address_status.go +++ b/pkg/resources/network/address_status.go @@ -37,7 +37,7 @@ type AddressStatusSpec struct { Flags nethelpers.AddressFlags `yaml:"flags"` } -// NewAddressStatus initializes a SecretsStatus resource. +// NewAddressStatus initializes a AddressStatus resource. func NewAddressStatus(namespace resource.Namespace, id resource.ID) *AddressStatus { r := &AddressStatus{ md: resource.NewMetadata(namespace, AddressStatusType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/hostname_spec.go b/pkg/resources/network/hostname_spec.go new file mode 100644 index 0000000000..1c35e412b5 --- /dev/null +++ b/pkg/resources/network/hostname_spec.go @@ -0,0 +1,118 @@ +// 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 network + +import ( + "fmt" + "strings" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" +) + +// HostnameSpecType is type of HostnameSpec resource. +const HostnameSpecType = resource.Type("HostnameSpecs.net.talos.dev") + +// HostnameSpec resource holds node hostname. +type HostnameSpec struct { + md resource.Metadata + spec HostnameSpecSpec +} + +// HostnameID is the ID of the singleton instance. +const HostnameID resource.ID = "hostname" + +// HostnameSpecSpec describes node nostname. +type HostnameSpecSpec struct { + Hostname string `yaml:"hostname"` + Domainname string `yaml:"domainname"` + ConfigLayer ConfigLayer `yaml:"layer"` +} + +// Validate the hostname. +func (spec *HostnameSpecSpec) Validate() error { + lenHostname := len(spec.Hostname) + + if lenHostname == 0 || lenHostname > 63 { + return fmt.Errorf("invalid hostname %q", spec.Hostname) + } + + if len(spec.FQDN()) > 253 { + return fmt.Errorf("fqdn is too long: %d", len(spec.FQDN())) + } + + return nil +} + +// FQDN returns the fully-qualified domain name. +func (spec *HostnameSpecSpec) FQDN() string { + if spec.Domainname == "" { + return spec.Hostname + } + + return spec.Hostname + "." + spec.Domainname +} + +// ParseFQDN into parts and validate it. +func (spec *HostnameSpecSpec) ParseFQDN(fqdn string) error { + parts := strings.SplitN(fqdn, ".", 2) + + spec.Hostname = parts[0] + + if len(parts) > 1 { + spec.Domainname = parts[1] + } + + return spec.Validate() +} + +// NewHostnameSpec initializes a HostnameSpec resource. +func NewHostnameSpec(namespace resource.Namespace, id resource.ID) *HostnameSpec { + r := &HostnameSpec{ + md: resource.NewMetadata(namespace, HostnameSpecType, id, resource.VersionUndefined), + spec: HostnameSpecSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *HostnameSpec) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *HostnameSpec) Spec() interface{} { + return r.spec +} + +func (r *HostnameSpec) String() string { + return fmt.Sprintf("network.HostnameSpec(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *HostnameSpec) DeepCopy() resource.Resource { + return &HostnameSpec{ + md: r.md, + spec: r.spec, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *HostnameSpec) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: HostnameSpecType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *HostnameSpec) TypedSpec() *HostnameSpecSpec { + return &r.spec +} diff --git a/pkg/resources/network/hostname_status.go b/pkg/resources/network/hostname_status.go new file mode 100644 index 0000000000..151cc154c7 --- /dev/null +++ b/pkg/resources/network/hostname_status.go @@ -0,0 +1,94 @@ +// 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 network + +import ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" +) + +// HostnameStatusType is type of HostnameStatus resource. +const HostnameStatusType = resource.Type("HostnameStatuses.net.talos.dev") + +// HostnameStatus resource holds node hostname. +type HostnameStatus struct { + md resource.Metadata + spec HostnameStatusSpec +} + +// HostnameStatusSpec describes node nostname. +type HostnameStatusSpec struct { + Hostname string `yaml:"hostname"` + Domainname string `yaml:"domainname"` +} + +// FQDN returns the fully-qualified domain name. +func (spec *HostnameStatusSpec) FQDN() string { + if spec.Domainname == "" { + return spec.Hostname + } + + return spec.Hostname + "." + spec.Domainname +} + +// NewHostnameStatus initializes a HostnameStatus resource. +func NewHostnameStatus(namespace resource.Namespace, id resource.ID) *HostnameStatus { + r := &HostnameStatus{ + md: resource.NewMetadata(namespace, HostnameStatusType, id, resource.VersionUndefined), + spec: HostnameStatusSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *HostnameStatus) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *HostnameStatus) Spec() interface{} { + return r.spec +} + +func (r *HostnameStatus) String() string { + return fmt.Sprintf("network.HostnameStatus(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *HostnameStatus) DeepCopy() resource.Resource { + return &HostnameStatus{ + md: r.md, + spec: r.spec, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *HostnameStatus) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: HostnameStatusType, + Aliases: []resource.Type{"hostname"}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Hostname", + JSONPath: "{.hostname}", + }, + { + Name: "Domainname", + JSONPath: "{.domainname}", + }, + }, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *HostnameStatus) TypedSpec() *HostnameStatusSpec { + return &r.spec +} diff --git a/pkg/resources/network/link_refresh.go b/pkg/resources/network/link_refresh.go index 63f6f249bc..15697bbc0d 100644 --- a/pkg/resources/network/link_refresh.go +++ b/pkg/resources/network/link_refresh.go @@ -31,7 +31,7 @@ type LinkRefreshSpec struct { Generation int `yaml:"generation"` } -// NewLinkRefresh initializes a SecretsStatus resource. +// NewLinkRefresh initializes a LinkRefresh resource. func NewLinkRefresh(namespace resource.Namespace, id resource.ID) *LinkRefresh { r := &LinkRefresh{ md: resource.NewMetadata(namespace, LinkRefreshType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/link_spec.go b/pkg/resources/network/link_spec.go index 8291b6d113..6a9eb20e91 100644 --- a/pkg/resources/network/link_spec.go +++ b/pkg/resources/network/link_spec.go @@ -28,7 +28,7 @@ type LinkSpecSpec struct { Name string `yaml:"name"` // Logical describes if the interface should be created on the fly if it doesn't exist. - Logical bool `yaml:"dynamic"` + Logical bool `yaml:"logical"` // If Up is true, bring interface up, otherwise bring interface down. // @@ -101,7 +101,7 @@ func (spec *LinkSpecSpec) Merge(other *LinkSpecSpec) error { return nil } -// NewLinkSpec initializes a SecretsStatus resource. +// NewLinkSpec initializes a LinkSpec resource. func NewLinkSpec(namespace resource.Namespace, id resource.ID) *LinkSpec { r := &LinkSpec{ md: resource.NewMetadata(namespace, LinkSpecType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/link_status.go b/pkg/resources/network/link_status.go index 5908fc5711..5dd39f6459 100644 --- a/pkg/resources/network/link_status.go +++ b/pkg/resources/network/link_status.go @@ -48,7 +48,7 @@ type LinkStatusSpec struct { Wireguard WireguardSpec `yaml:"wireguard,omitempty"` } -// NewLinkStatus initializes a SecretsStatus resource. +// NewLinkStatus initializes a LinkStatus resource. func NewLinkStatus(namespace resource.Namespace, id resource.ID) *LinkStatus { r := &LinkStatus{ md: resource.NewMetadata(namespace, LinkStatusType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/network_test.go b/pkg/resources/network/network_test.go index 227ba5578d..f40369ff40 100644 --- a/pkg/resources/network/network_test.go +++ b/pkg/resources/network/network_test.go @@ -27,12 +27,18 @@ func TestRegisterResource(t *testing.T) { for _, resource := range []resource.Resource{ &network.AddressStatus{}, &network.AddressSpec{}, + &network.HostnameStatus{}, + &network.HostnameSpec{}, &network.LinkRefresh{}, &network.LinkStatus{}, &network.LinkSpec{}, &network.NodeAddress{}, + &network.ResolverStatus{}, + &network.ResolverSpec{}, &network.RouteStatus{}, &network.RouteSpec{}, + &network.TimeServerStatus{}, + &network.TimeServerSpec{}, } { assert.NoError(t, resourceRegistry.Register(ctx, resource)) } diff --git a/pkg/resources/network/node_address.go b/pkg/resources/network/node_address.go index d6bf77576b..e9c3216c19 100644 --- a/pkg/resources/network/node_address.go +++ b/pkg/resources/network/node_address.go @@ -42,7 +42,7 @@ type NodeAddressSpec struct { Addresses []netaddr.IP `yaml:"addresses"` } -// NewNodeAddress initializes a SecretsStatus resource. +// NewNodeAddress initializes a NodeAddress resource. func NewNodeAddress(namespace resource.Namespace, id resource.ID) *NodeAddress { r := &NodeAddress{ md: resource.NewMetadata(namespace, NodeAddressType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/resolver_spec.go b/pkg/resources/network/resolver_spec.go new file mode 100644 index 0000000000..23ce744034 --- /dev/null +++ b/pkg/resources/network/resolver_spec.go @@ -0,0 +1,83 @@ +// 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 network + +import ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "inet.af/netaddr" +) + +// ResolverSpecType is type of ResolverSpec resource. +const ResolverSpecType = resource.Type("ResolverSpecs.net.talos.dev") + +// ResolverSpec resource holds DNS resolver info. +type ResolverSpec struct { + md resource.Metadata + spec ResolverSpecSpec +} + +// ResolverID is the ID of the singleton instance. +const ResolverID resource.ID = "resolvers" + +// ResolverSpecSpec describes DNS resolvers. +type ResolverSpecSpec struct { + DNSServers []netaddr.IP `yaml:"dnsServers"` + ConfigLayer ConfigLayer `yaml:"layer"` +} + +// NewResolverSpec initializes a ResolverSpec resource. +func NewResolverSpec(namespace resource.Namespace, id resource.ID) *ResolverSpec { + r := &ResolverSpec{ + md: resource.NewMetadata(namespace, ResolverSpecType, id, resource.VersionUndefined), + spec: ResolverSpecSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *ResolverSpec) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *ResolverSpec) Spec() interface{} { + return r.spec +} + +func (r *ResolverSpec) String() string { + return fmt.Sprintf("network.ResolverSpec(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *ResolverSpec) DeepCopy() resource.Resource { + return &ResolverSpec{ + md: r.md, + spec: ResolverSpecSpec{ + DNSServers: append([]netaddr.IP(nil), r.spec.DNSServers...), + ConfigLayer: r.spec.ConfigLayer, + }, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *ResolverSpec) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: ResolverSpecType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *ResolverSpec) TypedSpec() *ResolverSpecSpec { + return &r.spec +} diff --git a/pkg/resources/network/resolver_status.go b/pkg/resources/network/resolver_status.go new file mode 100644 index 0000000000..eceb0365e1 --- /dev/null +++ b/pkg/resources/network/resolver_status.go @@ -0,0 +1,83 @@ +// 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 network + +import ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "inet.af/netaddr" +) + +// ResolverStatusType is type of ResolverStatus resource. +const ResolverStatusType = resource.Type("ResolverStatuses.net.talos.dev") + +// ResolverStatus resource holds DNS resolver info. +type ResolverStatus struct { + md resource.Metadata + spec ResolverStatusSpec +} + +// ResolverStatusSpec describes DNS resolvers. +type ResolverStatusSpec struct { + DNSServers []netaddr.IP `yaml:"dnsServers"` +} + +// NewResolverStatus initializes a ResolverStatus resource. +func NewResolverStatus(namespace resource.Namespace, id resource.ID) *ResolverStatus { + r := &ResolverStatus{ + md: resource.NewMetadata(namespace, ResolverStatusType, id, resource.VersionUndefined), + spec: ResolverStatusSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *ResolverStatus) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *ResolverStatus) Spec() interface{} { + return r.spec +} + +func (r *ResolverStatus) String() string { + return fmt.Sprintf("network.ResolverStatus(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *ResolverStatus) DeepCopy() resource.Resource { + return &ResolverStatus{ + md: r.md, + spec: ResolverStatusSpec{ + DNSServers: append([]netaddr.IP(nil), r.spec.DNSServers...), + }, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *ResolverStatus) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: ResolverStatusType, + Aliases: []resource.Type{"resolvers"}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Resolvers", + JSONPath: "{.dnsServers}", + }, + }, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *ResolverStatus) TypedSpec() *ResolverStatusSpec { + return &r.spec +} diff --git a/pkg/resources/network/route_spec.go b/pkg/resources/network/route_spec.go index 582398513c..0f3043467d 100644 --- a/pkg/resources/network/route_spec.go +++ b/pkg/resources/network/route_spec.go @@ -38,7 +38,7 @@ type RouteSpecSpec struct { ConfigLayer ConfigLayer `yaml:"layer"` } -// NewRouteSpec initializes a SecretsStatus resource. +// NewRouteSpec initializes a RouteSpec resource. func NewRouteSpec(namespace resource.Namespace, id resource.ID) *RouteSpec { r := &RouteSpec{ md: resource.NewMetadata(namespace, RouteSpecType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/route_status.go b/pkg/resources/network/route_status.go index f62eeb78e5..6dfaaf962b 100644 --- a/pkg/resources/network/route_status.go +++ b/pkg/resources/network/route_status.go @@ -39,7 +39,7 @@ type RouteStatusSpec struct { Protocol nethelpers.RouteProtocol `yaml:"protocol"` } -// NewRouteStatus initializes a SecretsStatus resource. +// NewRouteStatus initializes a RouteStatus resource. func NewRouteStatus(namespace resource.Namespace, id resource.ID) *RouteStatus { r := &RouteStatus{ md: resource.NewMetadata(namespace, RouteStatusType, id, resource.VersionUndefined), diff --git a/pkg/resources/network/timeserver_spec.go b/pkg/resources/network/timeserver_spec.go new file mode 100644 index 0000000000..7d320e1937 --- /dev/null +++ b/pkg/resources/network/timeserver_spec.go @@ -0,0 +1,82 @@ +// 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 network + +import ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" +) + +// TimeServerSpecType is type of TimeServerSpec resource. +const TimeServerSpecType = resource.Type("TimeServerSpecs.net.talos.dev") + +// TimeServerSpec resource holds NTP server info. +type TimeServerSpec struct { + md resource.Metadata + spec TimeServerSpecSpec +} + +// TimeServerID is the ID of the singleton instance. +const TimeServerID resource.ID = "timeservers" + +// TimeServerSpecSpec describes NTP servers. +type TimeServerSpecSpec struct { + NTPServers []string `yaml:"timeServers"` + ConfigLayer ConfigLayer `yaml:"layer"` +} + +// NewTimeServerSpec initializes a TimeServerSpec resource. +func NewTimeServerSpec(namespace resource.Namespace, id resource.ID) *TimeServerSpec { + r := &TimeServerSpec{ + md: resource.NewMetadata(namespace, TimeServerSpecType, id, resource.VersionUndefined), + spec: TimeServerSpecSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *TimeServerSpec) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *TimeServerSpec) Spec() interface{} { + return r.spec +} + +func (r *TimeServerSpec) String() string { + return fmt.Sprintf("network.TimeServerSpec(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *TimeServerSpec) DeepCopy() resource.Resource { + return &TimeServerSpec{ + md: r.md, + spec: TimeServerSpecSpec{ + NTPServers: append([]string(nil), r.spec.NTPServers...), + ConfigLayer: r.spec.ConfigLayer, + }, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *TimeServerSpec) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: TimeServerSpecType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *TimeServerSpec) TypedSpec() *TimeServerSpecSpec { + return &r.spec +} diff --git a/pkg/resources/network/timeserver_status.go b/pkg/resources/network/timeserver_status.go new file mode 100644 index 0000000000..2253b54477 --- /dev/null +++ b/pkg/resources/network/timeserver_status.go @@ -0,0 +1,82 @@ +// 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 network + +import ( + "fmt" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" +) + +// TimeServerStatusType is type of TimeServerStatus resource. +const TimeServerStatusType = resource.Type("TimeServerStatuses.net.talos.dev") + +// TimeServerStatus resource holds NTP server info. +type TimeServerStatus struct { + md resource.Metadata + spec TimeServerStatusSpec +} + +// TimeServerStatusSpec describes NTP servers. +type TimeServerStatusSpec struct { + NTPServers []string `yaml:"timeServers"` +} + +// NewTimeServerStatus initializes a TimeServerStatus resource. +func NewTimeServerStatus(namespace resource.Namespace, id resource.ID) *TimeServerStatus { + r := &TimeServerStatus{ + md: resource.NewMetadata(namespace, TimeServerStatusType, id, resource.VersionUndefined), + spec: TimeServerStatusSpec{}, + } + + r.md.BumpVersion() + + return r +} + +// Metadata implements resource.Resource. +func (r *TimeServerStatus) Metadata() *resource.Metadata { + return &r.md +} + +// Spec implements resource.Resource. +func (r *TimeServerStatus) Spec() interface{} { + return r.spec +} + +func (r *TimeServerStatus) String() string { + return fmt.Sprintf("network.TimeServerStatus(%q)", r.md.ID()) +} + +// DeepCopy implements resource.Resource. +func (r *TimeServerStatus) DeepCopy() resource.Resource { + return &TimeServerStatus{ + md: r.md, + spec: TimeServerStatusSpec{ + NTPServers: append([]string(nil), r.spec.NTPServers...), + }, + } +} + +// ResourceDefinition implements meta.ResourceDefinitionProvider interface. +func (r *TimeServerStatus) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: TimeServerStatusType, + Aliases: []resource.Type{"timeserver", "timeservers"}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Timeservers", + JSONPath: "{.timeServers}", + }, + }, + } +} + +// TypedSpec allows to access the Spec with the proper type. +func (r *TimeServerStatus) TypedSpec() *TimeServerStatusSpec { + return &r.spec +}