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:
@ -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()
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
40
pkg/machinery/extensions/config.go
Normal file
40
pkg/machinery/extensions/config.go
Normal 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
|
||||
}
|
6
pkg/machinery/extensions/extensions.go
Normal file
6
pkg/machinery/extensions/extensions.go
Normal 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
|
Reference in New Issue
Block a user