From 64487625f1fcc132292b8b44c38d659af5d8f6a4 Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Mon, 1 May 2023 23:58:14 +0530 Subject: [PATCH] feat: udevd rules controller Use a controller for udevd rules. Signed-off-by: Noel Georgi --- .golangci.yml | 6 + api/resource/definitions/files/files.proto | 10 + .../controllers/cri/seccomp_profile_test.go | 35 +-- .../pkg/controllers/files/etcfile_test.go | 100 ++----- .../pkg/controllers/files/udev_rule.go | 120 ++++++++ .../pkg/controllers/files/udev_rule_file.go | 126 +++++++++ .../pkg/controllers/files/udev_rule_test.go | 109 ++++++++ .../pkg/runtime/v1alpha1/v1alpha1_runtime.go | 1 + .../runtime/v1alpha1/v1alpha1_sequencer.go | 2 +- .../v1alpha1/v1alpha1_sequencer_tasks.go | 48 ++-- .../runtime/v1alpha2/v1alpha2_controller.go | 7 + .../pkg/runtime/v1alpha2/v1alpha2_state.go | 2 + .../resource/definitions/files/files.pb.go | 147 +++++++++- .../definitions/files/files_vtproto.pb.go | 264 ++++++++++++++++++ .../resources/files/deep_copy.generated.go | 14 +- pkg/machinery/resources/files/etcfile_spec.go | 2 +- pkg/machinery/resources/files/files_test.go | 2 + pkg/machinery/resources/files/udev_rule.go | 57 ++++ .../resources/files/udev_rule_status.go | 57 ++++ website/content/v1.5/reference/api.md | 32 +++ 20 files changed, 999 insertions(+), 142 deletions(-) create mode 100644 internal/app/machined/pkg/controllers/files/udev_rule.go create mode 100644 internal/app/machined/pkg/controllers/files/udev_rule_file.go create mode 100644 internal/app/machined/pkg/controllers/files/udev_rule_test.go create mode 100644 pkg/machinery/resources/files/udev_rule.go create mode 100644 pkg/machinery/resources/files/udev_rule_status.go diff --git a/.golangci.yml b/.golangci.yml index a73f34fecc..18018df55a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -202,6 +202,12 @@ issues: - path: internal/app/machined/pkg/system/services linters: - dupl + - path: internal/app/machined/pkg/controllers/files + linters: + - dupl + - path: resources/files + linters: + - dupl - path: cmd/installer/pkg/qemuimg text: "should have a package comment" linters: diff --git a/api/resource/definitions/files/files.proto b/api/resource/definitions/files/files.proto index bb671bd2a5..deb8943836 100755 --- a/api/resource/definitions/files/files.proto +++ b/api/resource/definitions/files/files.proto @@ -15,3 +15,13 @@ message EtcFileStatusSpec { string spec_version = 1; } +// UdevRuleSpec is the specification for UdevRule resource. +message UdevRuleSpec { + string rule = 1; +} + +// UdevRuleStatusSpec is the specification for UdevRule resource. +message UdevRuleStatusSpec { + bool active = 1; +} + diff --git a/internal/app/machined/pkg/controllers/cri/seccomp_profile_test.go b/internal/app/machined/pkg/controllers/cri/seccomp_profile_test.go index 174e4c076f..8dfe932d5c 100644 --- a/internal/app/machined/pkg/controllers/cri/seccomp_profile_test.go +++ b/internal/app/machined/pkg/controllers/cri/seccomp_profile_test.go @@ -5,6 +5,7 @@ package cri_test import ( + "fmt" "testing" "time" @@ -84,29 +85,6 @@ func (suite *CRISeccompProfileSuite) TestReconcileSeccompProfile() { }) } - suite.AssertWithin(1*time.Second, 100*time.Millisecond, func() error { - seccompProfile, err := ctest.Get[*criseccompresource.SeccompProfile]( - suite, - criseccompresource.NewSeccompProfile("audit.json").Metadata(), - ) - if err != nil { - if state.IsNotFoundError(err) { - return retry.ExpectedError(err) - } - - return err - } - - spec := seccompProfile.TypedSpec() - - suite.Assert().Equal("audit.json", spec.Name) - suite.Assert().Equal(map[string]interface{}{ - "defaultAction": "SCMP_ACT_LOG", - }, spec.Value) - - return nil - }) - // test deletion cfg = config.NewMachineConfig(&v1alpha1.Config{ MachineConfig: &v1alpha1.MachineConfig{ @@ -123,9 +101,8 @@ func (suite *CRISeccompProfileSuite) TestReconcileSeccompProfile() { }, }) - ctest.UpdateWithConflicts(suite, cfg, func(mc *config.MachineConfig) error { - return nil - }) + cfg.Metadata().SetVersion(cfg.Metadata().Version().Next()) + suite.Require().NoError(suite.State().Update(suite.Ctx(), cfg)) suite.AssertWithin(1*time.Second, 100*time.Millisecond, func() error { _, err := ctest.Get[*criseccompresource.SeccompProfile]( @@ -133,14 +110,14 @@ func (suite *CRISeccompProfileSuite) TestReconcileSeccompProfile() { criseccompresource.NewSeccompProfile("deny.json").Metadata(), ) if err != nil { - if !state.IsNotFoundError(err) { - return err + if state.IsNotFoundError(err) { + return nil } return err } - return nil + return retry.ExpectedError(fmt.Errorf("seccomp profile with id deny.json should not exist")) }) } diff --git a/internal/app/machined/pkg/controllers/files/etcfile_test.go b/internal/app/machined/pkg/controllers/files/etcfile_test.go index a577c507c3..a0fd75679a 100644 --- a/internal/app/machined/pkg/controllers/files/etcfile_test.go +++ b/internal/app/machined/pkg/controllers/files/etcfile_test.go @@ -5,76 +5,49 @@ package files_test import ( - "context" - "log" "os" "path/filepath" "strconv" - "sync" "testing" "time" - "github.com/cosi-project/runtime/pkg/controller/runtime" "github.com/cosi-project/runtime/pkg/resource" "github.com/cosi-project/runtime/pkg/state" - "github.com/cosi-project/runtime/pkg/state/impl/inmem" - "github.com/cosi-project/runtime/pkg/state/impl/namespaced" "github.com/siderolabs/go-retry/retry" "github.com/stretchr/testify/suite" + "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest" filesctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/files" - "github.com/siderolabs/talos/pkg/logging" "github.com/siderolabs/talos/pkg/machinery/resources/files" ) type EtcFileSuite struct { - suite.Suite - - state state.State - - runtime *runtime.Runtime - wg sync.WaitGroup - - ctx context.Context //nolint:containedctx - ctxCancel context.CancelFunc - + ctest.DefaultSuite etcPath string shadowPath string } -func (suite *EtcFileSuite) 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.startRuntime() +func TestEtcFileSuite(t *testing.T) { + // skip test if we are not root + if os.Getuid() != 0 { + t.Skip("can't run the test as non-root") + } - suite.etcPath = suite.T().TempDir() - suite.shadowPath = suite.T().TempDir() + etcTempPath := t.TempDir() + shadowTempPath := t.TempDir() - suite.Require().NoError( - suite.runtime.RegisterController( - &filesctrl.EtcFileController{ - EtcPath: suite.etcPath, - ShadowPath: suite.shadowPath, + suite.Run(t, &EtcFileSuite{ + DefaultSuite: ctest.DefaultSuite{ + AfterSetup: func(suite *ctest.DefaultSuite) { + suite.Require().NoError(suite.Runtime().RegisterController(&filesctrl.EtcFileController{ + EtcPath: etcTempPath, + ShadowPath: shadowTempPath, + })) }, - ), - ) -} - -func (suite *EtcFileSuite) startRuntime() { - suite.wg.Add(1) - - go func() { - defer suite.wg.Done() - - suite.Assert().NoError(suite.runtime.Run(suite.ctx)) - }() + }, + etcPath: etcTempPath, + shadowPath: shadowTempPath, + }) } func (suite *EtcFileSuite) assertEtcFile(filename, contents string, expectedVersion resource.Version) error { @@ -87,8 +60,8 @@ func (suite *EtcFileSuite) assertEtcFile(filename, contents string, expectedVers return retry.ExpectedErrorf("contents don't match %q != %q", string(b), contents) } - r, err := suite.state.Get( - suite.ctx, + r, err := suite.State().Get( + suite.Ctx(), resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, filename, resource.VersionUndefined), ) if err != nil { @@ -123,19 +96,15 @@ func (suite *EtcFileSuite) TestFiles() { suite.T().Logf("mock created %q", filepath.Join(suite.etcPath, etcFileSpec.Metadata().ID())) suite.Require().NoError(os.WriteFile(filepath.Join(suite.etcPath, etcFileSpec.Metadata().ID()), nil, 0o644)) - suite.Require().NoError(suite.state.Create(suite.ctx, etcFileSpec)) + suite.Require().NoError(suite.State().Create(suite.Ctx(), etcFileSpec)) - suite.Assert().NoError( - retry.Constant(5*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( - func() error { - return suite.assertEtcFile("test1", "foo", etcFileSpec.Metadata().Version()) - }, - ), - ) + suite.AssertWithin(5*time.Second, 100*time.Millisecond, func() error { + return suite.assertEtcFile("test1", "foo", etcFileSpec.Metadata().Version()) + }) for _, r := range []resource.Resource{etcFileSpec} { for { - ready, err := suite.state.Teardown(suite.ctx, r.Metadata()) + ready, err := suite.State().Teardown(suite.Ctx(), r.Metadata()) suite.Require().NoError(err) if ready { @@ -146,18 +115,3 @@ func (suite *EtcFileSuite) TestFiles() { } } } - -func (suite *EtcFileSuite) TearDownTest() { - suite.T().Log("tear down") - - suite.ctxCancel() - - suite.wg.Wait() - - // trigger updates in resources to stop watch loops - suite.Assert().NoError(suite.state.Create(context.Background(), files.NewEtcFileSpec(files.NamespaceName, "bar"))) -} - -func TestEtcFileSuite(t *testing.T) { - suite.Run(t, new(EtcFileSuite)) -} diff --git a/internal/app/machined/pkg/controllers/files/udev_rule.go b/internal/app/machined/pkg/controllers/files/udev_rule.go new file mode 100644 index 0000000000..7a89ff0473 --- /dev/null +++ b/internal/app/machined/pkg/controllers/files/udev_rule.go @@ -0,0 +1,120 @@ +// 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 files + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/martinlindhe/base36" + "github.com/siderolabs/go-pointer" + "go.uber.org/zap" + + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/files" +) + +// UdevRuleController is a controller that generates udev rules. +type UdevRuleController struct{} + +// Name implements controller.Controller interface. +func (ctrl *UdevRuleController) Name() string { + return "files.UdevRuleController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *UdevRuleController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: config.MachineConfigType, + ID: pointer.To(config.V1Alpha1ID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *UdevRuleController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: files.UdevRuleType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +// nolint:gocyclo +func (ctrl *UdevRuleController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + cfg, err := safe.ReaderGet[*config.MachineConfig](ctx, r, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined)) + if err != nil { + if state.IsNotFoundError(err) { + continue + } + + return fmt.Errorf("error getting config: %w", err) + } + + touchedIDs := make(map[string]struct{}, len(cfg.Config().Machine().Udev().Rules())) + + for _, rule := range cfg.Config().Machine().Udev().Rules() { + ruleID := ctrl.generateRuleHash(rule) + + if err = safe.WriterModify(ctx, r, files.NewUdevRule(ruleID), func(udevRule *files.UdevRule) error { + udevRule.TypedSpec().Rule = rule + + return nil + }); err != nil { + return err + } + + touchedIDs[ruleID] = struct{}{} + } + + // list keys for cleanup + list, err := safe.ReaderList[*files.UdevRule](ctx, r, resource.NewMetadata(files.NamespaceName, files.UdevRuleType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing udev rules: %w", err) + } + + for iter := safe.IteratorFromList(list); iter.Next(); { + rule := iter.Value() + + if _, ok := touchedIDs[rule.Metadata().ID()]; !ok { + if err := r.Destroy(ctx, rule.Metadata()); err != nil { + return fmt.Errorf("error deleting udev rule %s: %w", rule.Metadata().ID(), err) + } + } + } + + r.ResetRestartBackoff() + } +} + +func (ctrl *UdevRuleController) generateRuleHash(rule string) string { + h := sha256.New() + h.Write([]byte(rule)) + + hashBytes := h.Sum(nil) + + b36 := strings.ToLower(base36.EncodeBytes(hashBytes)) + + return b36[:8] +} diff --git a/internal/app/machined/pkg/controllers/files/udev_rule_file.go b/internal/app/machined/pkg/controllers/files/udev_rule_file.go new file mode 100644 index 0000000000..75e112660c --- /dev/null +++ b/internal/app/machined/pkg/controllers/files/udev_rule_file.go @@ -0,0 +1,126 @@ +// 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 files + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "go.uber.org/zap" + + runtimetalos "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" + "github.com/siderolabs/talos/pkg/machinery/resources/files" +) + +// UdevRuleFileController is a controller for UdevRule files. +type UdevRuleFileController struct { + V1Alpha1Mode runtimetalos.Mode + UdevRulesFile string + CommandRunner func(ctx context.Context, name string, args ...string) (string, error) +} + +// Name implements controller.Controller interface. +func (ctrl *UdevRuleFileController) Name() string { + return "files.UdevRuleFileController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *UdevRuleFileController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: files.NamespaceName, + Type: files.UdevRuleType, + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *UdevRuleFileController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: files.UdevRuleStatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +// +// nolint:gocyclo +func (ctrl *UdevRuleFileController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // udev rules has no effect in container mode, so skip it. + if ctrl.V1Alpha1Mode == runtimetalos.ModeContainer { + continue + } + + list, err := safe.ReaderList[*files.UdevRule](ctx, r, resource.NewMetadata(files.NamespaceName, files.UdevRuleType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("failed to list udev rules: %w", err) + } + + if list.Len() == 0 { + if err = os.RemoveAll(ctrl.UdevRulesFile); err != nil { + return fmt.Errorf("failed to remove custom udev rules: %w", err) + } + + continue + } + + var content strings.Builder + + for iter := safe.IteratorFromList(list); iter.Next(); { + rule := iter.Value().TypedSpec().Rule + + content.WriteString(strings.ReplaceAll(rule, "\n", "\\\n")) + content.WriteByte('\n') + } + + if err = os.WriteFile(ctrl.UdevRulesFile, []byte(content.String()), 0o644); err != nil { + return fmt.Errorf("failed writing custom udev rules: %w", err) + } + + if _, err := ctrl.CommandRunner(ctx, "/sbin/udevadm", "control", "--reload"); err != nil { + return err + } + + if _, err := ctrl.CommandRunner(ctx, "/sbin/udevadm", "trigger", "--type=devices", "--action=add"); err != nil { + return err + } + + if _, err := ctrl.CommandRunner(ctx, "/sbin/udevadm", "trigger", "--type=subsystems", "--action=add"); err != nil { + return err + } + + // This ensures that `udevd` finishes processing kernel events, triggered by + // `udevd trigger`, to prevent a race condition when a user specifies a path + // under `/dev/disk/*` in any disk definitions. + if _, err := ctrl.CommandRunner(ctx, "/sbin/udevadm", "settle", "--timeout=50"); err != nil { + return err + } + + if err := safe.WriterModify(ctx, r, files.NewUdevRuleStatus("udev"), func(rule *files.UdevRuleStatus) error { + rule.TypedSpec().Active = true + + return nil + }); err != nil { + return fmt.Errorf("failed to update udev rule status: %w", err) + } + + r.ResetRestartBackoff() + } +} diff --git a/internal/app/machined/pkg/controllers/files/udev_rule_test.go b/internal/app/machined/pkg/controllers/files/udev_rule_test.go new file mode 100644 index 0000000000..2704b098d4 --- /dev/null +++ b/internal/app/machined/pkg/controllers/files/udev_rule_test.go @@ -0,0 +1,109 @@ +// 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 files_test + +import ( + "fmt" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/go-retry/retry" + "github.com/stretchr/testify/suite" + + "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest" + filesctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/files" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/files" +) + +func (suite *UdevRuleSuite) TestUdevRule() { + cfg := config.NewMachineConfig(&v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineUdev: &v1alpha1.UdevConfig{ + UdevRules: []string{ + `SUBSYSTEM=="block", KERNEL=="vdb*", SYMLINK+="myhdda%n"`, + `SUBSYSTEM=="block", KERNEL=="vdb*", SYMLINK+="myhddb%n"`, + }, + }, + }, + }) + + suite.Require().NoError(suite.State().Create(suite.Ctx(), cfg)) + + for _, tt := range []struct { + // id is the first 8 characters of the base36 encoded sha256 hash of the rule + id string + expected string + }{ + { + id: "168vxb2k", + expected: `SUBSYSTEM=="block", KERNEL=="vdb*", SYMLINK+="myhdda%n"`, + }, + { + id: "3aaseddz", + expected: `SUBSYSTEM=="block", KERNEL=="vdb*", SYMLINK+="myhddb%n"`, + }, + } { + suite.AssertWithin(3*time.Second, 100*time.Millisecond, func() error { + udevRule, err := ctest.Get[*files.UdevRule](suite, files.NewUdevRule(tt.id).Metadata()) + if err != nil { + if state.IsNotFoundError(err) { + return retry.ExpectedError(err) + } + + return err + } + + spec := udevRule.TypedSpec() + + suite.Assert().Equal(tt.expected, spec.Rule) + + return nil + }) + } + + // test deletion + cfg = config.NewMachineConfig(&v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineUdev: &v1alpha1.UdevConfig{ + UdevRules: []string{ + `SUBSYSTEM=="block", KERNEL=="vdb*", SYMLINK+="myhdda%n"`, + }, + }, + }, + }) + + cfg.Metadata().SetVersion(cfg.Metadata().Version().Next()) + suite.Require().NoError(suite.State().Update(suite.Ctx(), cfg)) + + suite.AssertWithin(1*time.Second, 100*time.Millisecond, func() error { + _, err := ctest.Get[*files.UdevRule](suite, files.NewUdevRule("3aaseddz").Metadata()) + if err != nil { + if state.IsNotFoundError(err) { + return nil + } + + return err + } + + return retry.ExpectedError(fmt.Errorf("udev rule with id 3aaseddz should not exist")) + }) +} + +func TestUdevRuleSuite(t *testing.T) { + suite.Run(t, &UdevRuleSuite{ + DefaultSuite: ctest.DefaultSuite{ + AfterSetup: func(suite *ctest.DefaultSuite) { + suite.Require().NoError(suite.Runtime().RegisterController(&filesctrl.UdevRuleController{})) + }, + }, + }) +} + +type UdevRuleSuite struct { + ctest.DefaultSuite +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go index e950aa232f..4f00ea7b0c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go @@ -153,6 +153,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error { newConfig.MachineConfig.MachinePods = currentConfig.MachineConfig.MachinePods newConfig.MachineConfig.MachineSeccompProfiles = currentConfig.MachineConfig.MachineSeccompProfiles newConfig.MachineConfig.MachineNodeLabels = currentConfig.MachineConfig.MachineNodeLabels + newConfig.MachineConfig.MachineUdev = currentConfig.MachineConfig.MachineUdev if newConfig.MachineConfig.MachineFeatures != nil && currentConfig.MachineConfig.MachineFeatures != nil { newConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = currentConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go index 71ced0fe5b..d2496a6910 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go @@ -265,7 +265,7 @@ func (*Sequencer) Boot(r runtime.Runtime) []runtime.Phase { "legacyCleanup", CleanupLegacyStaticPodFiles, ).AppendWhen( - r.State().Platform().Mode() != runtime.ModeContainer, + r.State().Platform().Mode() != runtime.ModeContainer && len(r.Config().Machine().Udev().Rules()) > 0, "udevSetup", WriteUdevRules, ).AppendWhen( diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go index 5293956080..76e146a47a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go @@ -767,44 +767,38 @@ func StartContainerd(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) } // WriteUdevRules is the task that writes udev rules to a udev rules file. -// TODO: frezbo: move this to controller based since writing udev rules doesn't need a restart. func WriteUdevRules(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { - rules := r.Config().Machine().Udev().Rules() + // wait for the udev rules loaded status to appear + st := r.State().V1Alpha2().Resources() - var content strings.Builder + // limit overall waiting time + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() - for _, rule := range rules { - content.WriteString(strings.ReplaceAll(rule, "\n", "\\\n")) - content.WriteByte('\n') + ch := make(chan state.Event) + if err = st.Watch(ctx, resourcefiles.NewUdevRuleStatus("udev").Metadata(), ch); err != nil { + return fmt.Errorf("failed to watch udev rules status: %w", err) } - if err = os.WriteFile(constants.UdevRulesPath, []byte(content.String()), 0o644); err != nil { - return fmt.Errorf("failed writing custom udev rules: %w", err) - } + // wait for the udev rules to be loaded - if len(rules) > 0 { - if _, err := cmd.RunContext(ctx, "/sbin/udevadm", "control", "--reload"); err != nil { - return err - } - - if _, err := cmd.RunContext(ctx, "/sbin/udevadm", "trigger", "--type=devices", "--action=add"); err != nil { - return err - } + select { + case <-ch: + case <-ctx.Done(): + return ctx.Err() + } - if _, err := cmd.RunContext(ctx, "/sbin/udevadm", "trigger", "--type=subsystems", "--action=add"); err != nil { - return err + _, err = st.WatchFor(ctx, resourcefiles.NewUdevRuleStatus("udev").Metadata(), state.WithCondition(func(r resource.Resource) (bool, error) { + udevRuleStatus, ok := r.(*resourcefiles.UdevRuleStatus) + if !ok { + return false, nil } - // This ensures that `udevd` finishes processing kernel events, triggered by - // `udevd trigger`, to prevent a race condition when a user specifies a path - // under `/dev/disk/*` in any disk definitions. - _, err := cmd.RunContext(ctx, "/sbin/udevadm", "settle", "--timeout=50") + return udevRuleStatus.TypedSpec().Active, nil + })) - return err - } - - return nil + return err }, "writeUdevRules" } diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index 314b197a6d..5806a95059 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -16,6 +16,7 @@ import ( "github.com/cosi-project/runtime/pkg/resource" "github.com/cosi-project/runtime/pkg/state" "github.com/siderolabs/gen/slices" + "github.com/siderolabs/go-cmd/pkg/cmd" "github.com/siderolabs/go-procfs/procfs" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -114,6 +115,12 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error EtcPath: "/etc", ShadowPath: constants.SystemEtcPath, }, + &files.UdevRuleController{}, + &files.UdevRuleFileController{ + V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), + UdevRulesFile: constants.UdevRulesPath, + CommandRunner: cmd.RunContext, + }, &hardware.SystemInfoController{ V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index 184304577e..96d12311ee 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -109,6 +109,8 @@ func NewState() (*State, error) { &etcd.Member{}, &files.EtcFileSpec{}, &files.EtcFileStatus{}, + &files.UdevRule{}, + &files.UdevRuleStatus{}, &hardware.Processor{}, &hardware.MemoryModule{}, &hardware.SystemInformation{}, diff --git a/pkg/machinery/api/resource/definitions/files/files.pb.go b/pkg/machinery/api/resource/definitions/files/files.pb.go index 91e0c05ad7..05fcf4bc5a 100644 --- a/pkg/machinery/api/resource/definitions/files/files.pb.go +++ b/pkg/machinery/api/resource/definitions/files/files.pb.go @@ -125,6 +125,102 @@ func (x *EtcFileStatusSpec) GetSpecVersion() string { return "" } +// UdevRuleSpec is the specification for UdevRule resource. +type UdevRuleSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` +} + +func (x *UdevRuleSpec) Reset() { + *x = UdevRuleSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_definitions_files_files_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UdevRuleSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UdevRuleSpec) ProtoMessage() {} + +func (x *UdevRuleSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_files_files_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UdevRuleSpec.ProtoReflect.Descriptor instead. +func (*UdevRuleSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_files_files_proto_rawDescGZIP(), []int{2} +} + +func (x *UdevRuleSpec) GetRule() string { + if x != nil { + return x.Rule + } + return "" +} + +// UdevRuleStatusSpec is the specification for UdevRule resource. +type UdevRuleStatusSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Active bool `protobuf:"varint,1,opt,name=active,proto3" json:"active,omitempty"` +} + +func (x *UdevRuleStatusSpec) Reset() { + *x = UdevRuleStatusSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_definitions_files_files_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UdevRuleStatusSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UdevRuleStatusSpec) ProtoMessage() {} + +func (x *UdevRuleStatusSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_files_files_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UdevRuleStatusSpec.ProtoReflect.Descriptor instead. +func (*UdevRuleStatusSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_files_files_proto_rawDescGZIP(), []int{3} +} + +func (x *UdevRuleStatusSpec) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + var File_resource_definitions_files_files_proto protoreflect.FileDescriptor var file_resource_definitions_files_files_proto_rawDesc = []byte{ @@ -140,12 +236,17 @@ var file_resource_definitions_files_files_proto_rawDesc = []byte{ 0x11, 0x45, 0x74, 0x63, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x70, 0x65, 0x63, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, - 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, - 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, - 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x22, 0x0a, 0x0c, 0x55, 0x64, 0x65, 0x76, 0x52, 0x75, 0x6c, + 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x75, 0x6c, 0x65, 0x22, 0x2c, 0x0a, 0x12, 0x55, 0x64, 0x65, + 0x76, 0x52, 0x75, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, + 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, + 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x69, + 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -160,10 +261,12 @@ func file_resource_definitions_files_files_proto_rawDescGZIP() []byte { return file_resource_definitions_files_files_proto_rawDescData } -var file_resource_definitions_files_files_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_resource_definitions_files_files_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_resource_definitions_files_files_proto_goTypes = []interface{}{ - (*EtcFileSpecSpec)(nil), // 0: talos.resource.definitions.files.EtcFileSpecSpec - (*EtcFileStatusSpec)(nil), // 1: talos.resource.definitions.files.EtcFileStatusSpec + (*EtcFileSpecSpec)(nil), // 0: talos.resource.definitions.files.EtcFileSpecSpec + (*EtcFileStatusSpec)(nil), // 1: talos.resource.definitions.files.EtcFileStatusSpec + (*UdevRuleSpec)(nil), // 2: talos.resource.definitions.files.UdevRuleSpec + (*UdevRuleStatusSpec)(nil), // 3: talos.resource.definitions.files.UdevRuleStatusSpec } var file_resource_definitions_files_files_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -203,6 +306,30 @@ func file_resource_definitions_files_files_proto_init() { return nil } } + file_resource_definitions_files_files_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UdevRuleSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_definitions_files_files_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UdevRuleStatusSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -210,7 +337,7 @@ func file_resource_definitions_files_files_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_definitions_files_files_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/machinery/api/resource/definitions/files/files_vtproto.pb.go b/pkg/machinery/api/resource/definitions/files/files_vtproto.pb.go index cff93a4b78..15626ff614 100644 --- a/pkg/machinery/api/resource/definitions/files/files_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/files/files_vtproto.pb.go @@ -104,6 +104,89 @@ func (m *EtcFileStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *UdevRuleSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UdevRuleSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *UdevRuleSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Rule) > 0 { + i -= len(m.Rule) + copy(dAtA[i:], m.Rule) + i = encodeVarint(dAtA, i, uint64(len(m.Rule))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *UdevRuleStatusSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UdevRuleStatusSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *UdevRuleStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Active { + i-- + if m.Active { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarint(dAtA []byte, offset int, v uint64) int { offset -= sov(v) base := offset @@ -146,6 +229,33 @@ func (m *EtcFileStatusSpec) SizeVT() (n int) { return n } +func (m *UdevRuleSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Rule) + if l > 0 { + n += 1 + l + sov(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *UdevRuleStatusSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Active { + n += 2 + } + n += len(m.unknownFields) + return n +} + func sov(x uint64) (n int) { return (bits.Len64(x|1) + 6) / 7 } @@ -339,6 +449,160 @@ func (m *EtcFileStatusSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *UdevRuleSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UdevRuleSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UdevRuleSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Rule", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Rule = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *UdevRuleStatusSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UdevRuleStatusSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UdevRuleStatusSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Active", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Active = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skip(dAtA []byte) (n int, err error) { l := len(dAtA) diff --git a/pkg/machinery/resources/files/deep_copy.generated.go b/pkg/machinery/resources/files/deep_copy.generated.go index cd5b23e54a..2ccfe3b21a 100644 --- a/pkg/machinery/resources/files/deep_copy.generated.go +++ b/pkg/machinery/resources/files/deep_copy.generated.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "deep-copy -type EtcFileSpecSpec -type EtcFileStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type EtcFileSpecSpec -type EtcFileStatusSpec -type UdevRuleSpec -type UdevRuleStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package files @@ -21,3 +21,15 @@ func (o EtcFileStatusSpec) DeepCopy() EtcFileStatusSpec { var cp EtcFileStatusSpec = o return cp } + +// DeepCopy generates a deep copy of UdevRuleSpec. +func (o UdevRuleSpec) DeepCopy() UdevRuleSpec { + var cp UdevRuleSpec = o + return cp +} + +// DeepCopy generates a deep copy of UdevRuleStatusSpec. +func (o UdevRuleStatusSpec) DeepCopy() UdevRuleStatusSpec { + var cp UdevRuleStatusSpec = o + return cp +} diff --git a/pkg/machinery/resources/files/etcfile_spec.go b/pkg/machinery/resources/files/etcfile_spec.go index c0178a858a..a1b0a425a3 100644 --- a/pkg/machinery/resources/files/etcfile_spec.go +++ b/pkg/machinery/resources/files/etcfile_spec.go @@ -15,7 +15,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/proto" ) -//go:generate deep-copy -type EtcFileSpecSpec -type EtcFileStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate deep-copy -type EtcFileSpecSpec -type EtcFileStatusSpec -type UdevRuleSpec -type UdevRuleStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . // EtcFileSpecType is type of EtcFile resource. const EtcFileSpecType = resource.Type("EtcFileSpecs.files.talos.dev") diff --git a/pkg/machinery/resources/files/files_test.go b/pkg/machinery/resources/files/files_test.go index ab0802e207..b82e6d78ce 100644 --- a/pkg/machinery/resources/files/files_test.go +++ b/pkg/machinery/resources/files/files_test.go @@ -27,6 +27,8 @@ func TestRegisterResource(t *testing.T) { for _, resource := range []resource.Resource{ &files.EtcFileSpec{}, &files.EtcFileStatus{}, + &files.UdevRule{}, + &files.UdevRuleStatus{}, } { assert.NoError(t, resourceRegistry.Register(ctx, resource)) } diff --git a/pkg/machinery/resources/files/udev_rule.go b/pkg/machinery/resources/files/udev_rule.go new file mode 100644 index 0000000000..7fbb6dca19 --- /dev/null +++ b/pkg/machinery/resources/files/udev_rule.go @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package files + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/talos/pkg/machinery/proto" +) + +// UdevRuleType is type of UdevRules resource. +const UdevRuleType = resource.Type("UdevRules.files.talos.dev") + +// UdevRule is a resource for UdevRule. +type UdevRule = typed.Resource[UdevRuleSpec, UdevRuleRD] + +// UdevRuleSpec is the specification for UdevRule resource. +// +//gotagsrewrite:gen +type UdevRuleSpec struct { + Rule string `yaml:"rule" protobuf:"1"` +} + +// NewUdevRule initializes a new UdevRule resource. +func NewUdevRule(id string) *UdevRule { + return typed.NewResource[UdevRuleSpec, UdevRuleRD]( + resource.NewMetadata(NamespaceName, UdevRuleType, id, resource.VersionUndefined), + UdevRuleSpec{}, + ) +} + +// UdevRuleRD provides auxiliary methods for UdevRules. +type UdevRuleRD struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (UdevRuleRD) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: UdevRuleType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[UdevRuleSpec](UdevRuleType, &UdevRule{}) + if err != nil { + panic(err) + } +} diff --git a/pkg/machinery/resources/files/udev_rule_status.go b/pkg/machinery/resources/files/udev_rule_status.go new file mode 100644 index 0000000000..01a9650e29 --- /dev/null +++ b/pkg/machinery/resources/files/udev_rule_status.go @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package files + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/talos/pkg/machinery/proto" +) + +// UdevRuleStatusType is type of UdevRules resource. +const UdevRuleStatusType = resource.Type("UdevRuleStatuses.files.talos.dev") + +// UdevRuleStatus is a resource for UdevRule. +type UdevRuleStatus = typed.Resource[UdevRuleStatusSpec, UdevRuleStatusRD] + +// UdevRuleStatusSpec is the specification for UdevRule resource. +// +//gotagsrewrite:gen +type UdevRuleStatusSpec struct { + Active bool `yaml:"active" protobuf:"1"` +} + +// NewUdevRuleStatus initializes a new UdevRule resource. +func NewUdevRuleStatus(id string) *UdevRuleStatus { + return typed.NewResource[UdevRuleStatusSpec, UdevRuleStatusRD]( + resource.NewMetadata(NamespaceName, UdevRuleStatusType, id, resource.VersionUndefined), + UdevRuleStatusSpec{}, + ) +} + +// UdevRuleStatusRD provides auxiliary methods for UdevRules. +type UdevRuleStatusRD struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (UdevRuleStatusRD) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: UdevRuleStatusType, + Aliases: []resource.Type{}, + DefaultNamespace: NamespaceName, + PrintColumns: []meta.PrintColumn{}, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[UdevRuleStatusSpec](UdevRuleStatusType, &UdevRuleStatus{}) + if err != nil { + panic(err) + } +} diff --git a/website/content/v1.5/reference/api.md b/website/content/v1.5/reference/api.md index b0218b3785..f5a2cb3e17 100644 --- a/website/content/v1.5/reference/api.md +++ b/website/content/v1.5/reference/api.md @@ -79,6 +79,8 @@ description: Talos gRPC API reference. - [resource/definitions/files/files.proto](#resource/definitions/files/files.proto) - [EtcFileSpecSpec](#talos.resource.definitions.files.EtcFileSpecSpec) - [EtcFileStatusSpec](#talos.resource.definitions.files.EtcFileStatusSpec) + - [UdevRuleSpec](#talos.resource.definitions.files.UdevRuleSpec) + - [UdevRuleStatusSpec](#talos.resource.definitions.files.UdevRuleStatusSpec) - [resource/definitions/hardware/hardware.proto](#resource/definitions/hardware/hardware.proto) - [MemoryModuleSpec](#talos.resource.definitions.hardware.MemoryModuleSpec) @@ -1560,6 +1562,36 @@ EtcFileStatusSpec describes status of rendered secrets. + + + +### UdevRuleSpec +UdevRuleSpec is the specification for UdevRule resource. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| rule | [string](#string) | | | + + + + + + + + +### UdevRuleStatusSpec +UdevRuleStatusSpec is the specification for UdevRule resource. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| active | [bool](#bool) | | | + + + + +