Skip to content

Commit

Permalink
Add ignition-liveapply entrypoint
Browse files Browse the repository at this point in the history
This transforms the Ignition binary to support the multicall convention.
For maximum backwards compatibility, we default to the usual Ignition
functionality without requiring an exact match. The first multicall use
after `ignition` is `ignition-liveapply`, which is the main goal of this
patch.

This new entrypoint takes an Ignition config and applies it to the
current rootfs (or one provided by `-root`). This tool could then be
used as part of build processes where we want to bake an Ignition
config, such as in CoreOS layering:

https://github.com/coreos/enhancements/blob/main/os/coreos-layering.md

This in turn should also help drain from the MCO the re-implementation
of large parts of the Ignition spec.

Another approach would be to build on top of `ignition -stage=files`,
but in the end I think we want a cleaner dedicated UX for this workflow
long-term (and in fact we should clean up the hacky places where we call
Ignition like this).

It shouldn't be used by end-users to live apply changes to their
Ignition config. A check that we are in a container is added as a
safeguard against this.

By default, unsupported modifications (like partitioning) trigger
errors, though one may pass `-ignore-unsupported` to override this. Also
by default, remote resources are automatically fetched, though one may
pass `-offline` to override this.
  • Loading branch information
jlebon committed Feb 2, 2022
1 parent 02c1c63 commit 9132ad0
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 13 deletions.
9 changes: 9 additions & 0 deletions config/util/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ func IsTrue(b *bool) bool {
func IsFalse(b *bool) bool {
return b != nil && !*b
}

func StrSliceContains(slice []string, s string) bool {
for _, e := range slice {
if e == s {
return true
}
}
return false
}
8 changes: 8 additions & 0 deletions internal/exec/stages/disks/disks.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ func isNoOp(config types.Config) bool {
len(config.Storage.Luks) == 0
}

func (s stage) ApplyLive(config types.Config) error {
// in theory, we could support this, but for now we don't need it
if isNoOp(config) {
return nil
}
return stages.ErrUnsupportedLive
}

func (s stage) Run(config types.Config) error {
// Interacting with disks/partitions/raids/filesystems in general can cause
// udev races. If we do not need to do anything, we also do not need to
Expand Down
4 changes: 4 additions & 0 deletions internal/exec/stages/fetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func (stage) Name() string {
return name
}

func (s stage) ApplyLive(_ types.Config) error {
return nil
}

func (s stage) Run(_ types.Config) error {
// Nothing - all we do is fetch and allow anything else in the initramfs to run
s.Logger.Info("fetch complete")
Expand Down
4 changes: 4 additions & 0 deletions internal/exec/stages/fetch_offline/fetch-offline.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (stage) Name() string {
return name
}

func (s stage) ApplyLive(cfg types.Config) error {
return nil
}

func (s stage) Run(cfg types.Config) error {
if needsNet, err := configNeedsNet(&cfg); err != nil {
return err
Expand Down
38 changes: 25 additions & 13 deletions internal/exec/stages/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,29 @@ func (creator) Name() string {
type stage struct {
util.Util
toRelabel []string
isLive bool
}

func (stage) Name() string {
return name
}

func (s stage) ApplyLive(config types.Config) error {
s.isLive = true
return s.Run(config)
}

func (s stage) Run(config types.Config) error {
if err := s.checkRelabeling(); err != nil {
return fmt.Errorf("failed to check if SELinux labeling required: %v", err)
}
if !s.isLive {
if err := s.checkRelabeling(); err != nil {
return fmt.Errorf("failed to check if SELinux labeling required: %v", err)
}

if err := s.createPasswd(config); err != nil {
return fmt.Errorf("failed to create users/groups: %v", err)
// theoretically could support this, but the main user (CoreOS
// layering) does not
if err := s.createPasswd(config); err != nil {
return fmt.Errorf("failed to create users/groups: %v", err)
}
}

if err := s.createFilesystemsEntries(config); err != nil {
Expand All @@ -83,16 +93,18 @@ func (s stage) Run(config types.Config) error {
return fmt.Errorf("failed to create units: %v", err)
}

if err := s.createCrypttabEntries(config); err != nil {
return fmt.Errorf("creating crypttab entries: %v", err)
}
if !s.isLive {
if err := s.createCrypttabEntries(config); err != nil {
return fmt.Errorf("creating crypttab entries: %v", err)
}

if err := s.createResultFile(); err != nil {
return fmt.Errorf("creating result file: %v", err)
}
if err := s.createResultFile(); err != nil {
return fmt.Errorf("creating result file: %v", err)
}

if err := s.relabelFiles(); err != nil {
return fmt.Errorf("failed to handle relabeling: %v", err)
if err := s.relabelFiles(); err != nil {
return fmt.Errorf("failed to handle relabeling: %v", err)
}
}

return nil
Expand Down
7 changes: 7 additions & 0 deletions internal/exec/stages/kargs/kargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ func isNoOp(config types.Config) bool {
len(config.KernelArguments.ShouldNotExist) == 0
}

func (s stage) ApplyLive(config types.Config) error {
if isNoOp(config) {
return nil
}
return stages.ErrUnsupportedLive
}

func (s stage) Run(config types.Config) error {
if isNoOp(config) {
return nil
Expand Down
7 changes: 7 additions & 0 deletions internal/exec/stages/mount/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ func (stage) Name() string {
return name
}

func (s stage) ApplyLive(config types.Config) error {
if len(config.Storage.Filesystems) == 0 {
return nil
}
return stages.ErrUnsupportedLive
}

func (s stage) Run(config types.Config) error {
fss := []types.Filesystem{}
for _, fs := range config.Storage.Filesystems {
Expand Down
7 changes: 7 additions & 0 deletions internal/exec/stages/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@
package stages

import (
"errors"

"github.com/coreos/ignition/v2/config/v3_4_experimental/types"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/registry"
"github.com/coreos/ignition/v2/internal/resource"
"github.com/coreos/ignition/v2/internal/state"
)

var (
ErrUnsupportedLive = errors.New("cannot apply live")
)

// Stage is responsible for actually executing a stage of the configuration.
type Stage interface {
Run(config types.Config) error
ApplyLive(config types.Config) error
Name() string
}

Expand Down
7 changes: 7 additions & 0 deletions internal/exec/stages/umount/umount.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ func (stage) Name() string {
return name
}

func (s stage) ApplyLive(config types.Config) error {
if len(config.Storage.Filesystems) == 0 {
return nil
}
return stages.ErrUnsupportedLive
}

func (s stage) Run(config types.Config) error {
fss := []types.Filesystem{}
for _, fs := range config.Storage.Filesystems {
Expand Down
119 changes: 119 additions & 0 deletions internal/liveapply/liveapply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2021 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package liveapply

import (
"errors"
"fmt"
"os"
"strings"

"github.com/coreos/ignition/v2/config/util"
"github.com/coreos/ignition/v2/internal/exec"
"github.com/coreos/ignition/v2/internal/exec/stages"
_ "github.com/coreos/ignition/v2/internal/exec/stages/disks"
_ "github.com/coreos/ignition/v2/internal/exec/stages/fetch"
_ "github.com/coreos/ignition/v2/internal/exec/stages/fetch_offline"
_ "github.com/coreos/ignition/v2/internal/exec/stages/files"
_ "github.com/coreos/ignition/v2/internal/exec/stages/kargs"
_ "github.com/coreos/ignition/v2/internal/exec/stages/mount"
_ "github.com/coreos/ignition/v2/internal/exec/stages/umount"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/resource"
"github.com/coreos/ignition/v2/internal/state"

"github.com/coreos/ignition/v2/config/v3_4_experimental/types"
)

type Flags struct {
Root string
IgnoreUnsupported bool
Offline bool
}

func inContainer() bool {
if val, _ := os.LookupEnv("container"); val != "" {
return true
}

paths := []string{"/run/.containerenv", "/.dockerenv"}
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return true
}
}

return false
}

func Run(cfg types.Config, flags Flags, logger *log.Logger) error {
if !inContainer() {
return errors.New("this tool is not designed to run on a host system; reprovision the machine instead")
}

fetcher := resource.Fetcher{
Logger: logger,
Offline: flags.Offline,
}

state := state.State{}
cfgFetcher := exec.ConfigFetcher{
Logger: logger,
Fetcher: &fetcher,
State: &state,
}

finalCfg, err := cfgFetcher.RenderConfig(cfg)
if err != nil {
return err
}

// verify upfront if we'll need networking but we're not allowed
if flags.Offline {
stage := stages.Get("fetch-offline").Create(logger, flags.Root, fetcher, &state)
if err := stage.Run(finalCfg); err != nil {
return err
}
}

// Order in which to apply live. This is overkill since effectively only
// `files` supports it right now, but let's be extensible. Also ensures that
// all stages are accounted for.
stagesOrder := []string{"fetch-offline", "fetch", "kargs", "disks", "mount", "files", "umount"}
allStages := stages.Names()
if len(stagesOrder) != len(allStages) {
panic(fmt.Sprintf("%v != %v", stagesOrder, allStages))
}

for _, stageName := range stagesOrder {
if !util.StrSliceContains(allStages, stageName) {
panic(fmt.Sprintf("stage '%s' invalid", stageName))
}
if strings.HasPrefix(stageName, "fetch") {
continue // already fetched
}
stage := stages.Get(stageName).Create(logger, flags.Root, fetcher, &state)
if err := stage.ApplyLive(finalCfg); err != nil {
if err == stages.ErrUnsupportedLive && flags.IgnoreUnsupported {
logger.Info("running stage '%s': ignoring unsupported: %v", stageName, err)
continue
} else {
return fmt.Errorf("running stage '%s': %w", stageName, err)
}
}
}

return nil
}
69 changes: 69 additions & 0 deletions internal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"

"github.com/coreos/ignition/v2/config"
"github.com/coreos/ignition/v2/internal/exec"
"github.com/coreos/ignition/v2/internal/exec/stages"
_ "github.com/coreos/ignition/v2/internal/exec/stages/disks"
Expand All @@ -29,13 +32,23 @@ import (
_ "github.com/coreos/ignition/v2/internal/exec/stages/kargs"
_ "github.com/coreos/ignition/v2/internal/exec/stages/mount"
_ "github.com/coreos/ignition/v2/internal/exec/stages/umount"
"github.com/coreos/ignition/v2/internal/liveapply"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/platform"
"github.com/coreos/ignition/v2/internal/state"
"github.com/coreos/ignition/v2/internal/version"
)

func main() {
if filepath.Base(os.Args[0]) == "ignition-liveapply" {
ignitionLiveApplyMain()
} else {
// otherwise, assume regular Ignition
ignitionMain()
}
}

func ignitionMain() {
flags := struct {
configCache string
fetchTimeout time.Duration
Expand Down Expand Up @@ -117,3 +130,59 @@ func main() {
}
logger.Info("Ignition finished successfully")
}

func ignitionLiveApplyMain() {
printVersion := false
flags := liveapply.Flags{}
flag.BoolVar(&printVersion, "version", false, "print the version of ignition-liveapply")
flag.StringVar(&flags.Root, "root", "/", "root of the filesystem")
flag.BoolVar(&flags.IgnoreUnsupported, "ignore-unsupported", false, "ignore unsupported config sections")
flag.BoolVar(&flags.Offline, "offline", false, "error out if config references remote resources")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage:\n ignition-liveapply [flags] config.ign\n\n")
flag.PrintDefaults()
}

flag.Parse()

if printVersion {
fmt.Printf("%s\n", version.String)
return
}

if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
cfgArg := flag.Arg(0)

logger := log.New(true)
defer logger.Close()

logger.Info(version.String)

var blob []byte
var err error
if cfgArg == "-" {
blob, err = ioutil.ReadAll(os.Stdin)
} else {
// XXX: could in the future support fetching directly from HTTP(S) + `-checksum|-insecure` ?
blob, err = ioutil.ReadFile(cfgArg)
}
if err != nil {
logger.Crit("couldn't read config: %v", err)
os.Exit(1)
}

cfg, rpt, err := config.Parse(blob)
logger.LogReport(rpt)
if rpt.IsFatal() || err != nil {
logger.Crit("couldn't parse config: %v", err)
os.Exit(1)
}

if err := liveapply.Run(cfg, flags, &logger); err != nil {
logger.Crit("failed to liveapply: %v", err)
os.Exit(1)
}
}

0 comments on commit 9132ad0

Please sign in to comment.