Skip to content

Commit

Permalink
feat: udevd rules controller
Browse files Browse the repository at this point in the history
Use a controller for udevd rules.

Signed-off-by: Noel Georgi <[email protected]>
  • Loading branch information
frezbo committed May 2, 2023
1 parent f8a7a5b commit 6448762
Show file tree
Hide file tree
Showing 20 changed files with 999 additions and 142 deletions.
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions api/resource/definitions/files/files.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

35 changes: 6 additions & 29 deletions internal/app/machined/pkg/controllers/cri/seccomp_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package cri_test

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -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{
Expand All @@ -123,24 +101,23 @@ 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](
suite,
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"))
})
}

Expand Down
100 changes: 27 additions & 73 deletions internal/app/machined/pkg/controllers/files/etcfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
120 changes: 120 additions & 0 deletions internal/app/machined/pkg/controllers/files/udev_rule.go
Original file line number Diff line number Diff line change
@@ -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]
}
Loading

0 comments on commit 6448762

Please sign in to comment.