Andrey Smirnov d4b8445935
feat: support CRI configuration merging and reimplement registry config
Containerd doesn't support merging plugin configuration from multiple
sources, and Talos has several pieces which configure CRI plugin:
(see https://github.com/containerd/containerd/issues/5837)

* base config
* registry mirror config
* system extensions
* ...

So we implement our own simple way of merging config parts (by simply
concatenating text files) to build a final `cri.toml`.

At the same time containerd migrated to a new format to specify registry
mirror configuration, while old way (via CRI config) is going to be
removed in 1.7.0. New way also allows to apply most of registry
configuration (except for auth) on the fly.

Also, containerd was updated to 1.6.0-rc.0 and runc to 1.1.0.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
2022-01-20 23:05:20 +03:00

191 lines
5.5 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 (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"go.uber.org/zap"
"golang.org/x/sys/unix"
"github.com/talos-systems/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]interface{}
}
// 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]interface{})
}
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
list, err := r.List(ctx, 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.Items {
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 _, item := range list.Items {
spec := item.(*files.EtcFileSpec) //nolint:errcheck,forcetypeassert
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 = os.WriteFile(dst, spec.TypedSpec().Contents, spec.TypedSpec().Mode); err != nil {
return fmt.Errorf("error updating %q: %w", dst, err)
}
if err = r.Modify(ctx, files.NewEtcFileStatus(files.NamespaceName, filename), func(r resource.Resource) error {
r.(*files.EtcFileStatus).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
list, err = r.List(ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
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)
}
}
}
}
}
// 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
}