diff --git a/.golangci.yml b/.golangci.yml
index a73f34fecca..18018df55a5 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 bb671bd2a5a..deb89438367 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 174e4c076fc..8dfe932d5cb 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 a577c507c30..a0fd75679af 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 00000000000..7a89ff0473e
--- /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 00000000000..9c33c1b12ba
--- /dev/null
+++ b/internal/app/machined/pkg/controllers/files/udev_rule_file.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 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)
+ }
+
+ 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 00000000000..2704b098d41
--- /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 e950aa232fc..4f00ea7b0c2 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_tasks.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go
index 5293956080f..76e146a47a1 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 314b197a6da..5806a950593 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 184304577e9..96d12311eea 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 91e0c05ad76..05fcf4bc5a1 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 cff93a4b78d..15626ff6149 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 cd5b23e54a9..2ccfe3b21a7 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 c0178a858a1..a1b0a425a36 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 ab0802e2071..b82e6d78cee 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 00000000000..7fbb6dca194
--- /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 00000000000..01a9650e29e
--- /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 b0218b3785a..f5a2cb3e17f 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) | | |
+
+
+
+
+