talos/internal/app/machined/pkg/controllers/files/etcfile.go
Dmitry Sharshakov e899fb37fd
feat: label created files in /etc
Implement SELinux labeling support in EtcFileController, label both squashfs and runtime-created files in /etc and /system/etc.

Add corresponding test cases.

Signed-off-by: Dmitry Sharshakov <dmitry.sharshakov@siderolabs.com>
2024-11-22 09:16:13 +01:00

211 lines
6.1 KiB
Go

// 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 (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"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"
"golang.org/x/sys/unix"
"github.com/siderolabs/talos/internal/pkg/selinux"
"github.com/siderolabs/talos/pkg/machinery/resources/files"
)
// EtcFileController watches EtcFileSpecs, creates/updates files.
type EtcFileController struct {
// Path to /etc directory, read-only filesystem.
EtcPath string
// Shadow path where actual file will be created and bind mounted into EtcdPath.
ShadowPath string
// Cache of bind mounts created.
bindMounts map[string]any
}
// Name implements controller.Controller interface.
func (ctrl *EtcFileController) Name() string {
return "files.EtcFileController"
}
// Inputs implements controller.Controller interface.
func (ctrl *EtcFileController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: files.NamespaceName,
Type: files.EtcFileSpecType,
Kind: controller.InputStrong,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *EtcFileController) Outputs() []controller.Output {
return []controller.Output{
{
Type: files.EtcFileStatusType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo,cyclop
func (ctrl *EtcFileController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
if ctrl.bindMounts == nil {
ctrl.bindMounts = make(map[string]any)
}
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
list, err := safe.ReaderList[*files.EtcFileSpec](ctx, r, resource.NewMetadata(files.NamespaceName, files.EtcFileSpecType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing specs: %w", err)
}
// add finalizers for all live resources
for res := range list.All() {
if res.Metadata().Phase() != resource.PhaseRunning {
continue
}
if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil {
return fmt.Errorf("error adding finalizer: %w", err)
}
}
touchedIDs := make(map[resource.ID]struct{})
for spec := range list.All() {
filename := spec.Metadata().ID()
_, mountExists := ctrl.bindMounts[filename]
src := filepath.Join(ctrl.ShadowPath, filename)
dst := filepath.Join(ctrl.EtcPath, filename)
switch spec.Metadata().Phase() {
case resource.PhaseTearingDown:
if mountExists {
logger.Debug("removing bind mount", zap.String("src", src), zap.String("dst", dst))
if err = unix.Unmount(dst, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to unmount bind mount %q: %w", dst, err)
}
delete(ctrl.bindMounts, filename)
}
logger.Debug("removing file", zap.String("src", src))
if err = os.Remove(src); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to remove %q: %w", src, err)
}
// now remove finalizer as the link was deleted
if err = r.RemoveFinalizer(ctx, spec.Metadata(), ctrl.Name()); err != nil {
return fmt.Errorf("error removing finalizer: %w", err)
}
case resource.PhaseRunning:
if !mountExists {
logger.Debug("creating bind mount", zap.String("src", src), zap.String("dst", dst))
if err = createBindMount(src, dst, spec.TypedSpec().Mode); err != nil {
return fmt.Errorf("failed to create shadow bind mount %q -> %q: %w", src, dst, err)
}
ctrl.bindMounts[filename] = struct{}{}
}
logger.Debug("writing file contents", zap.String("dst", dst), zap.Stringer("version", spec.Metadata().Version()))
if err = UpdateFile(dst, spec.TypedSpec().Contents, spec.TypedSpec().Mode, spec.TypedSpec().SelinuxLabel); err != nil {
return fmt.Errorf("error updating %q: %w", dst, err)
}
if err = safe.WriterModify(ctx, r, files.NewEtcFileStatus(files.NamespaceName, filename), func(r *files.EtcFileStatus) error {
r.TypedSpec().SpecVersion = spec.Metadata().Version().String()
return nil
}); err != nil {
return fmt.Errorf("error updating status: %w", err)
}
touchedIDs[filename] = struct{}{}
}
}
// list statuses for cleanup
statuses, err := safe.ReaderList[*files.EtcFileStatus](ctx, r, resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for res := range statuses.All() {
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
if err = r.Destroy(ctx, res.Metadata()); err != nil {
return fmt.Errorf("error cleaning up specs: %w", err)
}
}
}
r.ResetRestartBackoff()
}
}
// createBindMount creates a common way to create a writable source file with a
// bind mounted destination. This is most commonly used for well known files
// under /etc that need to be adjusted during startup.
func createBindMount(src, dst string, mode os.FileMode) (err error) {
if err = os.MkdirAll(filepath.Dir(src), 0o755); err != nil {
return err
}
var f *os.File
if f, err = os.OpenFile(src, os.O_WRONLY|os.O_CREATE, mode); err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
if err = unix.Mount(src, dst, "", unix.MS_BIND|unix.MS_RDONLY, ""); err != nil {
return fmt.Errorf("failed to create bind mount for %s: %w", dst, err)
}
return nil
}
// UpdateFile is like `os.WriteFile`, but it will only update the file if the
// contents have changed.
func UpdateFile(filename string, contents []byte, mode os.FileMode, selinuxLabel string) error {
oldContents, err := os.ReadFile(filename)
if err == nil && bytes.Equal(oldContents, contents) {
return selinux.SetLabel(filename, selinuxLabel)
}
err = os.WriteFile(filename, contents, mode)
if err != nil {
return err
}
return selinux.SetLabel(filename, selinuxLabel)
}