feat: install readonly overlay mounts during talos chroot sequence

The list of layers should come from the `/extensions.yaml` configuration
file.

Closes: https://github.com/talos-systems/talos/issues/4814

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
This commit is contained in:
Artem Chernyshev
2022-01-19 13:54:06 +03:00
parent 9ad5a67d21
commit 21f497b3e2
6 changed files with 159 additions and 13 deletions

View File

@ -11,16 +11,19 @@ import (
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"github.com/talos-systems/go-kmsg"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"
"gopkg.in/freddierice/go-losetup.v1"
"github.com/talos-systems/talos/internal/pkg/mount"
"github.com/talos-systems/talos/internal/pkg/mount/switchroot"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/machinery/extensions"
"github.com/talos-systems/talos/pkg/version"
)
@ -49,14 +52,7 @@ func run() (err error) {
log.Printf("booting Talos %s", version.Tag)
// Mount the rootfs.
log.Println("mounting the rootfs")
squashfs, err := mount.SquashfsMountPoints(constants.NewRoot)
if err != nil {
return err
}
if err = mount.Mount(squashfs); err != nil {
if err = mountRootFS(); err != nil {
return err
}
@ -92,6 +88,88 @@ func recovery() {
unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
}
//nolint:gocyclo
func mountRootFS() error {
log.Println("mounting the rootfs")
var extensionsConfig *extensions.Config
extensionsConfig, err := extensions.LoadConfig(constants.ExtensionsConfigFile)
if err != nil {
if !os.IsNotExist(err) {
return err
}
}
var squashfs *mount.Points
// if no extensions found use plain squashfs mount
if extensionsConfig == nil || len(extensionsConfig.Layers) == 0 {
squashfs, err = mount.SquashfsMountPoints(constants.NewRoot)
if err != nil {
return err
}
return mount.Mount(squashfs)
}
// otherwise compose overlay mounts
type layer struct {
name string
image string
}
layers := []layer{}
squashfs = mount.NewMountPoints()
// going in the inverse order as earlier layers are overlayed on top of the latter ones
for i := len(extensionsConfig.Layers) - 1; i >= 0; i-- {
layers = append(layers, layer{
name: fmt.Sprintf("layer%d", i),
image: extensionsConfig.Layers[i].Image,
})
}
layers = append(layers, layer{
name: "root",
image: "/" + constants.RootfsAsset,
})
overlays := make([]string, 0, len(layers))
for _, layer := range layers {
var dev losetup.Device
dev, err = losetup.Attach(layer.image, 0, true)
if err != nil {
return err
}
p := mount.NewMountPoint(dev.Path(), "/"+layer.name, "squashfs", unix.MS_RDONLY|unix.MS_I_VERSION, "", mount.WithPrefix(constants.ExtensionLayers), mount.WithFlags(mount.ReadOnly|mount.Shared))
overlays = append(overlays, p.Target())
squashfs.Set(layer.name, p)
}
if err = mount.Mount(squashfs); err != nil {
return err
}
overlay := mount.NewMountPoints()
overlay.Set(constants.NewRoot, mount.NewMountPoint(strings.Join(overlays, ":"), constants.NewRoot, "", unix.MS_I_VERSION, "", mount.WithFlags(mount.ReadOnly|mount.ReadonlyOverlay|mount.Shared)))
if err = mount.Mount(overlay); err != nil {
return err
}
if err = mount.Unmount(squashfs); err != nil {
return err
}
return nil
}
func main() {
defer recovery()

View File

@ -8,7 +8,6 @@ import (
"bufio"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
@ -186,7 +185,7 @@ type Points struct {
func NewMountPoint(source, target, fstype string, flags uintptr, data string, setters ...Option) *Point {
opts := NewDefaultOptions(setters...)
return &Point{
p := &Point{
source: source,
target: target,
fstype: fstype,
@ -194,6 +193,12 @@ func NewMountPoint(source, target, fstype string, flags uintptr, data string, se
data: data,
Options: opts,
}
if p.Prefix != "" {
p.target = filepath.Join(p.Prefix, p.target)
}
return p
}
// NewMountPoints initializes and returns a Points struct.
@ -231,8 +236,6 @@ func (p *Point) Data() string {
// Mount attempts to retry a mount on EBUSY. It will attempt a retry
// every 100 milliseconds over the course of 5 seconds.
func (p *Point) Mount() (err error) {
p.target = path.Join(p.Prefix, p.target)
for _, hook := range p.Options.PreMountHooks {
if err = hook(p); err != nil {
return err
@ -250,6 +253,8 @@ func (p *Point) Mount() (err error) {
switch {
case p.MountFlags.Check(Overlay):
err = mountRetry(overlay, p, false)
case p.MountFlags.Check(ReadonlyOverlay):
err = mountRetry(readonlyOverlay, p, false)
default:
err = mountRetry(mount, p, false)
}
@ -277,7 +282,6 @@ func (p *Point) Unmount() (err error) {
}
if mounted {
p.target = path.Join(p.Prefix, p.target)
if err = mountRetry(unmount, p, true); err != nil {
return err
}
@ -418,6 +422,15 @@ func overlay(p *Point) error {
return nil
}
func readonlyOverlay(p *Point) error {
opts := fmt.Sprintf("lowerdir=%s", p.source)
if err := unix.Mount("overlay", p.target, "overlay", p.flags, opts); err != nil {
return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err)
}
return nil
}
func ensureDirectory(target string) (err error) {
if _, err := os.Stat(target); os.IsNotExist(err) {
if err = os.MkdirAll(target, os.ModeDir); err != nil {

View File

@ -17,6 +17,9 @@ const (
// Overlay indicates that a the partition for a given mount point should be
// mounted using overlayfs.
Overlay
// ReadonlyOverlay indicates that a the partition for a given mount point should be
// mounted using multi-layer readonly overlay from multiple partitions given as sources.
ReadonlyOverlay
// SkipIfMounted is a flag for skipping mount if the mountpoint is already mounted.
SkipIfMounted
// SkipIfNoFilesystem is a flag for skipping formatting and mounting if the mountpoint has not filesystem.

View File

@ -86,6 +86,12 @@ const (
// NewRoot is the path where the switchroot target is mounted.
NewRoot = "/root"
// ExtensionLayers is the path where the extensions layers are stored.
ExtensionLayers = "/layers"
// ExtensionsConfigFile extensions layers configuration file name.
ExtensionsConfigFile = "/extensions.yaml"
// EFIPartitionLabel is the label of the partition to use for mounting at
// the boot path.
EFIPartitionLabel = "EFI"

View File

@ -0,0 +1,40 @@
// 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 extensions
import (
"os"
"gopkg.in/yaml.v3"
)
// Config specifies Talos installer extensions configuration.
type Config struct {
Layers []*Layer `yaml:"layers"`
}
// Layer defines overlay mount layer.
type Layer struct {
Image string `yaml:"image"`
}
// LoadConfig load extensions config from a file.
func LoadConfig(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() //nolint:errcheck
var extensions *Config
decoder := yaml.NewDecoder(f)
if err = decoder.Decode(&extensions); err != nil {
return nil, err
}
return extensions, nil
}

View File

@ -0,0 +1,6 @@
// 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 extensions contains Talos extensions specific API.
package extensions