mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-10 00:27:05 +02:00
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>
211 lines
6.1 KiB
Go
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)
|
|
}
|