From 5517bad3dbc6412b8ed86c132289bb6986270346 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Jul 2021 20:33:21 -0400 Subject: [PATCH 1/5] *: add general mechanism for persisting state between stages Serialize a struct to a JSON file in /run at the end of each stage and load it at the beginning of the next stage. This is intended to be an internal mechanism that can change incompatibly without warning. --- internal/exec/engine.go | 4 +- internal/exec/stages/disks/disks.go | 4 +- internal/exec/stages/fetch/fetch.go | 4 +- .../stages/fetch_offline/fetch-offline.go | 4 +- internal/exec/stages/files/files.go | 4 +- internal/exec/stages/kargs/kargs.go | 4 +- internal/exec/stages/mount/mount.go | 4 +- internal/exec/stages/stages.go | 3 +- internal/exec/stages/umount/umount.go | 4 +- internal/exec/util/util.go | 2 + internal/main.go | 13 +++++ internal/state/state.go | 56 +++++++++++++++++++ tests/filesystem.go | 3 +- 13 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 internal/state/state.go diff --git a/internal/exec/engine.go b/internal/exec/engine.go index 9d245274e..bfe0a040b 100644 --- a/internal/exec/engine.go +++ b/internal/exec/engine.go @@ -40,6 +40,7 @@ import ( "github.com/coreos/ignition/v2/internal/providers/cmdline" "github.com/coreos/ignition/v2/internal/providers/system" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" "github.com/coreos/ignition/v2/internal/util" "github.com/coreos/vcontext/report" @@ -69,6 +70,7 @@ type Engine struct { Root string PlatformConfig platform.Config Fetcher *resource.Fetcher + State *state.State fetchedConfigs []fetchedConfig } @@ -138,7 +140,7 @@ func (e Engine) Run(stageName string) error { defer e.Logger.PopPrefix() fullConfig := latest.Merge(baseConfig, latest.Merge(systemBaseConfig, cfg)) - err = stages.Get(stageName).Create(e.Logger, e.Root, *e.Fetcher).Run(fullConfig) + err = stages.Get(stageName).Create(e.Logger, e.Root, *e.Fetcher, e.State).Run(fullConfig) if err == resource.ErrNeedNet && stageName == "fetch-offline" { err = e.signalNeedNet() if err != nil { diff --git a/internal/exec/stages/disks/disks.go b/internal/exec/stages/disks/disks.go index 5d2237bc3..3908a762c 100644 --- a/internal/exec/stages/disks/disks.go +++ b/internal/exec/stages/disks/disks.go @@ -28,6 +28,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "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/internal/systemd" ) @@ -41,12 +42,13 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, f resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, Fetcher: f, + State: state, }, } } diff --git a/internal/exec/stages/fetch/fetch.go b/internal/exec/stages/fetch/fetch.go index 2b8b50216..3c65ecb8b 100644 --- a/internal/exec/stages/fetch/fetch.go +++ b/internal/exec/stages/fetch/fetch.go @@ -24,6 +24,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" ) const ( @@ -36,11 +37,12 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, _ resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, _ resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, + State: state, }, } } diff --git a/internal/exec/stages/fetch_offline/fetch-offline.go b/internal/exec/stages/fetch_offline/fetch-offline.go index 3bac0c851..92e3c1804 100644 --- a/internal/exec/stages/fetch_offline/fetch-offline.go +++ b/internal/exec/stages/fetch_offline/fetch-offline.go @@ -28,6 +28,7 @@ import ( executil "github.com/coreos/ignition/v2/internal/exec/util" "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/internal/util" ) @@ -41,11 +42,12 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, _ resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, _ resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: executil.Util{ DestDir: root, Logger: logger, + State: state, }, } } diff --git a/internal/exec/stages/files/files.go b/internal/exec/stages/files/files.go index 7c0dea4c1..a1bff9d79 100644 --- a/internal/exec/stages/files/files.go +++ b/internal/exec/stages/files/files.go @@ -25,6 +25,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" ) const ( @@ -41,12 +42,13 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, f resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, Fetcher: f, + State: state, }, } } diff --git a/internal/exec/stages/kargs/kargs.go b/internal/exec/stages/kargs/kargs.go index 31e7bcb7f..24bd047cf 100644 --- a/internal/exec/stages/kargs/kargs.go +++ b/internal/exec/stages/kargs/kargs.go @@ -24,6 +24,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" ) const ( @@ -36,12 +37,13 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, f resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, Fetcher: f, + State: state, }, } } diff --git a/internal/exec/stages/mount/mount.go b/internal/exec/stages/mount/mount.go index fe90ec455..d9fdcf771 100644 --- a/internal/exec/stages/mount/mount.go +++ b/internal/exec/stages/mount/mount.go @@ -33,6 +33,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" ) const ( @@ -45,11 +46,12 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, f resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, + State: state, }, } } diff --git a/internal/exec/stages/stages.go b/internal/exec/stages/stages.go index 271a5d377..7b1b6ea82 100644 --- a/internal/exec/stages/stages.go +++ b/internal/exec/stages/stages.go @@ -19,6 +19,7 @@ import ( "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" ) // Stage is responsible for actually executing a stage of the configuration. @@ -30,7 +31,7 @@ type Stage interface { // StageCreator is responsible for instantiating a particular stage given a // logger and root path under the root partition. type StageCreator interface { - Create(logger *log.Logger, root string, f resource.Fetcher) Stage + Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) Stage Name() string } diff --git a/internal/exec/stages/umount/umount.go b/internal/exec/stages/umount/umount.go index 7926ea750..e0b1d3ef4 100644 --- a/internal/exec/stages/umount/umount.go +++ b/internal/exec/stages/umount/umount.go @@ -27,6 +27,7 @@ import ( "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" "golang.org/x/sys/unix" ) @@ -41,11 +42,12 @@ func init() { type creator struct{} -func (creator) Create(logger *log.Logger, root string, f resource.Fetcher) stages.Stage { +func (creator) Create(logger *log.Logger, root string, f resource.Fetcher, state *state.State) stages.Stage { return &stage{ Util: util.Util{ DestDir: root, Logger: logger, + State: state, }, } } diff --git a/internal/exec/util/util.go b/internal/exec/util/util.go index c6955a3c5..a0149e1c0 100644 --- a/internal/exec/util/util.go +++ b/internal/exec/util/util.go @@ -20,6 +20,7 @@ import ( "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/resource" + "github.com/coreos/ignition/v2/internal/state" ) // Util encapsulates logging and destdir indirection for the util methods. @@ -27,6 +28,7 @@ type Util struct { DestDir string // directory prefix to use in applying fs paths. Fetcher resource.Fetcher *log.Logger + State *state.State } // SplitPath splits /a/b/c/d into [a, b, c, d] diff --git a/internal/main.go b/internal/main.go index c4e6af169..175efb4ad 100644 --- a/internal/main.go +++ b/internal/main.go @@ -31,6 +31,7 @@ import ( _ "github.com/coreos/ignition/v2/internal/exec/stages/umount" "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" ) @@ -43,6 +44,7 @@ func main() { platform platform.Name root string stage stages.Name + stateFile string version bool logToStdout bool }{} @@ -54,6 +56,7 @@ func main() { flag.Var(&flags.platform, "platform", fmt.Sprintf("current platform. %v", platform.Names())) flag.StringVar(&flags.root, "root", "/", "root of the filesystem") flag.Var(&flags.stage, "stage", fmt.Sprintf("execution stage. %v", stages.Names())) + flag.StringVar(&flags.stateFile, "state-file", "/run/ignition/state", "where to store internal state") flag.BoolVar(&flags.version, "version", false, "print the version and exit") flag.BoolVar(&flags.logToStdout, "log-to-stdout", false, "log to stdout instead of the system log when set") @@ -92,6 +95,11 @@ func main() { logger.Crit("failed to generate fetcher: %s", err) os.Exit(3) } + state, err := state.Load(flags.stateFile) + if err != nil { + logger.Crit("reading state: %s", err) + os.Exit(3) + } engine := exec.Engine{ Root: flags.root, FetchTimeout: flags.fetchTimeout, @@ -100,6 +108,7 @@ func main() { ConfigCache: flags.configCache, PlatformConfig: platformConfig, Fetcher: &fetcher, + State: &state, } err = engine.Run(flags.stage.String()) @@ -110,5 +119,9 @@ func main() { logger.Crit("Ignition failed: %v", err.Error()) os.Exit(1) } + if err := engine.State.Save(flags.stateFile); err != nil { + logger.Crit("writing state: %v", err) + os.Exit(1) + } logger.Info("Ignition finished successfully") } diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 000000000..25fef9676 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,56 @@ +// 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 state + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +type State struct { +} + +func Load(path string) (State, error) { + data, err := ioutil.ReadFile(path) + if os.IsNotExist(err) { + // valid; return empty struct + return State{}, nil + } else if err != nil { + return State{}, fmt.Errorf("reading state file: %w", err) + } + var state State + if err = json.Unmarshal(data, &state); err != nil { + return State{}, fmt.Errorf("parsing state file: %w", err) + } + return state, nil +} + +func (s *State) Save(path string) error { + data, err := json.Marshal(s) + if err != nil { + return fmt.Errorf("serializing state file: %w", err) + } + data = append(data, '\n') + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return fmt.Errorf("creating directory for state file: %w", err) + } + if err := ioutil.WriteFile(path, data, 0600); err != nil { + return fmt.Errorf("writing state file: %w", err) + } + return nil +} diff --git a/tests/filesystem.go b/tests/filesystem.go index 5cf77e92b..b8680bcd0 100644 --- a/tests/filesystem.go +++ b/tests/filesystem.go @@ -155,7 +155,8 @@ func runIgnition(t *testing.T, ctx context.Context, stage, root, cwd string, app args := []string{"-platform", "file", "-stage", stage, "-root", root, "-log-to-stdout", "-config-cache", filepath.Join(cwd, "ignition.json"), - "-neednet", filepath.Join(cwd, "neednet")} + "-neednet", filepath.Join(cwd, "neednet"), + "-state-file", filepath.Join(cwd, "state")} cmd := exec.CommandContext(ctx, "ignition", args...) if cmd == nil { return fmt.Errorf("exec.CommandContext() returned nil") From 65e9c1611128a3b4822a7ab10ebc565be0efc669 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Jul 2021 22:34:47 -0400 Subject: [PATCH 2/5] stages/disks: use State to persist keyfiles for files stage --- internal/distro/distro.go | 6 ++-- internal/exec/stages/disks/luks.go | 29 ++++++++++++------- .../exec/stages/files/filesystemEntries.go | 16 ++++------ internal/state/state.go | 5 ++++ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/internal/distro/distro.go b/internal/distro/distro.go index ef4a3e9bd..9f2a1bcb1 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -74,8 +74,7 @@ var ( // ".ssh/authorized_keys" ("false"). writeAuthorizedKeysFragment = "true" - luksInitramfsKeyFilePath = "/run/ignition/luks-keyfiles/" - luksRealRootKeyFilePath = "/etc/luks/" + luksRealRootKeyFilePath = "/etc/luks/" ) func DiskByIDDir() string { return diskByIDDir } @@ -113,8 +112,7 @@ func CryptsetupCmd() string { return cryptsetupCmd } func KargsCmd() string { return kargsCmd } -func LuksInitramfsKeyFilePath() string { return luksInitramfsKeyFilePath } -func LuksRealRootKeyFilePath() string { return luksRealRootKeyFilePath } +func LuksRealRootKeyFilePath() string { return luksRealRootKeyFilePath } func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) && !BlackboxTesting() } func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } diff --git a/internal/exec/stages/disks/luks.go b/internal/exec/stages/disks/luks.go index 649d098ed..77ecc24e2 100644 --- a/internal/exec/stages/disks/luks.go +++ b/internal/exec/stages/disks/luks.go @@ -25,7 +25,6 @@ import ( "os" "os/exec" "path" - "path/filepath" "strings" "github.com/coreos/ignition/v2/config/util" @@ -33,6 +32,8 @@ import ( "github.com/coreos/ignition/v2/internal/distro" execUtil "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/resource" + + "github.com/vincent-petithory/dataurl" ) var ( @@ -104,6 +105,8 @@ func (s *stage) createLuks(config types.Config) error { return err } + s.State.LuksPersistKeyFiles = make(map[string]string) + for _, luks := range config.Storage.Luks { // TODO: allow Ignition generated KeyFiles for // non-clevis devices that can be persisted. @@ -111,15 +114,17 @@ func (s *stage) createLuks(config types.Config) error { // track whether Ignition creates the KeyFile // so that it can be removed var ignitionCreatedKeyFile bool - // create keyfile inside of tmpfs, it will be copied to the - // sysroot by the files stage - if err := os.MkdirAll(distro.LuksInitramfsKeyFilePath(), 0700); err != nil { - return fmt.Errorf("creating directory for keyfile: %v", err) + // create keyfile, remove on the way out + keyFile, err := ioutil.TempFile("", "ignition-luks-") + if err != nil { + return fmt.Errorf("creating keyfile: %w", err) } - keyFilePath := filepath.Join(distro.LuksInitramfsKeyFilePath(), luks.Name) + keyFilePath := keyFile.Name() + keyFile.Close() + defer os.Remove(keyFilePath) devAlias := execUtil.DeviceAlias(*luks.Device) if util.NilOrEmpty(luks.KeyFile.Source) { - // create a keyfile + // generate keyfile contents key, err := randHex(4096) if err != nil { return fmt.Errorf("generating keyfile: %v", err) @@ -316,17 +321,21 @@ func (s *stage) createLuks(config types.Config) error { } } - // assume the user does not want a key file & remove it for clevis based devices if ignitionCreatedKeyFile && luks.Clevis.IsPresent() { + // assume the user does not want the generated key & remove it if _, err := s.Logger.LogCmd( exec.Command(distro.CryptsetupCmd(), "luksRemoveKey", devAlias, keyFilePath), "removing key file for %v", luks.Name, ); err != nil { return fmt.Errorf("removing key file from luks device: %v", err) } - if err := os.Remove(keyFilePath); err != nil { - return fmt.Errorf("removing key file: %v", err) + } else { + // store the key to be persisted into the real root + key, err := ioutil.ReadFile(keyFilePath) + if err != nil { + return fmt.Errorf("failed to read keyfile %q: %w", keyFilePath, err) } + s.State.LuksPersistKeyFiles[luks.Name] = dataurl.EncodeBytes(key) } } diff --git a/internal/exec/stages/files/filesystemEntries.go b/internal/exec/stages/files/filesystemEntries.go index d788085ca..84e4a64bc 100644 --- a/internal/exec/stages/files/filesystemEntries.go +++ b/internal/exec/stages/files/filesystemEntries.go @@ -16,7 +16,6 @@ package files import ( "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -70,12 +69,11 @@ func (s *stage) createCrypttabEntries(config types.Config) error { if !luks.Clevis.IsPresent() { keyfile = filepath.Join(distro.LuksRealRootKeyFilePath(), luks.Name) - // Copy keyfile from /run to sysroot - contents, err := ioutil.ReadFile(filepath.Join(distro.LuksInitramfsKeyFilePath(), luks.Name)) - if err != nil { - return fmt.Errorf("reading keyfile for %s: %v", luks.Name, err) + // Write keyfile into sysroot + contentsUri, ok := s.State.LuksPersistKeyFiles[luks.Name] + if !ok { + return fmt.Errorf("missing persisted keyfile for %s", luks.Name) } - contentsUri := dataurl.EncodeBytes(contents) keyfilePath, err := s.JoinPath(keyfile) if err != nil { return fmt.Errorf("building keyfile path: %v", err) @@ -127,11 +125,9 @@ func (s *stage) createCrypttabEntries(config types.Config) error { if err := s.createEntries(extrafiles); err != nil { return fmt.Errorf("adding luks related files: %v", err) } - // delete the entire keyfiles folder in /run/ so that the keyfiles are stored on + // delete the persisted keyfiles from state so that the keyfiles are stored on // only the root device which can be encrypted - if err := os.RemoveAll(distro.LuksInitramfsKeyFilePath()); err != nil { - return fmt.Errorf("removing initramfs keyfiles: %v", err) - } + s.State.LuksPersistKeyFiles = nil return nil } diff --git a/internal/state/state.go b/internal/state/state.go index 25fef9676..ab8f3b358 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -23,6 +23,11 @@ import ( ) type State struct { + // Key files generated during LUKS setup in disks stage, which need + // to be written out during files stage. files stage removes them + // from state afterward to avoid leaking the keys into the running + // system. + LuksPersistKeyFiles map[string]string `json:"luksPersistKeyFiles"` } func Load(path string) (State, error) { From bc579e1a6acf3bccb718d98cf490e3c2b060b80e Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Jul 2021 22:36:13 -0400 Subject: [PATCH 3/5] engine: persist fetched config summaries in State --- internal/exec/engine.go | 45 +++++++++++++++++------------------------ internal/state/state.go | 9 +++++++++ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/internal/exec/engine.go b/internal/exec/engine.go index bfe0a040b..04e22f03b 100644 --- a/internal/exec/engine.go +++ b/internal/exec/engine.go @@ -71,13 +71,6 @@ type Engine struct { PlatformConfig platform.Config Fetcher *resource.Fetcher State *state.State - fetchedConfigs []fetchedConfig -} - -type fetchedConfig struct { - kind string - source string - referenced bool } // Run executes the stage of the given name. It returns true if the stage @@ -95,10 +88,10 @@ func (e Engine) Run(stageName string) error { e.Logger.Crit("failed to acquire system base config: %v", err) return err } else if err == nil { - e.fetchedConfigs = append(e.fetchedConfigs, fetchedConfig{ - kind: "base", - source: "system", - referenced: false, + e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{ + Kind: "base", + Source: "system", + Referenced: false, }) } @@ -166,18 +159,18 @@ func (e Engine) Run(stageName string) error { // logStructuredJournalEntry logs information related to // a user/base config into the systemd journal log. -func logStructuredJournalEntry(cfgInfo fetchedConfig) error { +func logStructuredJournalEntry(cfgInfo state.FetchedConfig) error { ignitionInfo := map[string]string{ - "IGNITION_CONFIG_TYPE": cfgInfo.kind, - "IGNITION_CONFIG_SRC": cfgInfo.source, - "IGNITION_CONFIG_REFERENCED": strconv.FormatBool(cfgInfo.referenced), + "IGNITION_CONFIG_TYPE": cfgInfo.Kind, + "IGNITION_CONFIG_SRC": cfgInfo.Source, + "IGNITION_CONFIG_REFERENCED": strconv.FormatBool(cfgInfo.Referenced), "MESSAGE_ID": ignitionFetchedConfigMsgId, } referenced := "" - if cfgInfo.referenced { + if cfgInfo.Referenced { referenced = "referenced " } - msg := fmt.Sprintf("fetched %s%s config from %q", referenced, cfgInfo.kind, cfgInfo.source) + msg := fmt.Sprintf("fetched %s%s config from %q", referenced, cfgInfo.Kind, cfgInfo.Source) if err := journal.Send(msg, journal.PriInfo, ignitionInfo); err != nil { return err } @@ -195,7 +188,7 @@ func (e *Engine) acquireConfig(stageName string) (cfg types.Config, err error) { // if we've successfully fetched and cached the configs, log about them if err == nil { - for _, cfgInfo := range e.fetchedConfigs { + for _, cfgInfo := range e.State.FetchedConfigs { if logerr := logStructuredJournalEntry(cfgInfo); logerr != nil { e.Logger.Info("failed to log systemd journal entry: %v", logerr) } @@ -326,10 +319,10 @@ func (e *Engine) fetchProviderConfig() (types.Config, error) { return types.Config{}, err } - e.fetchedConfigs = append(e.fetchedConfigs, fetchedConfig{ - kind: "user", - source: providerKey, - referenced: false, + e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{ + Kind: "user", + Source: providerKey, + Referenced: false, }) // Replace the HTTP client in the fetcher to be configured with the @@ -431,10 +424,10 @@ func (e *Engine) fetchReferencedConfig(cfgRef types.Resource) (types.Config, err e.Logger.Debug("fetched referenced config from data url with SHA512: %s", hex.EncodeToString(hash[:])) } - e.fetchedConfigs = append(e.fetchedConfigs, fetchedConfig{ - kind: "user", - source: u.Path, - referenced: true, + e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{ + Kind: "user", + Source: u.Path, + Referenced: true, }) if err := util.AssertValid(cfgRef.Verification, rawCfg); err != nil { diff --git a/internal/state/state.go b/internal/state/state.go index ab8f3b358..347ee545b 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -23,6 +23,9 @@ import ( ) type State struct { + // Information about configs fetched by the fetch stages. Used + // when writing the result file in files stage. + FetchedConfigs []FetchedConfig `json:"fetchedConfigs"` // Key files generated during LUKS setup in disks stage, which need // to be written out during files stage. files stage removes them // from state afterward to avoid leaking the keys into the running @@ -30,6 +33,12 @@ type State struct { LuksPersistKeyFiles map[string]string `json:"luksPersistKeyFiles"` } +type FetchedConfig struct { + Kind string `json:"kind"` + Source string `json:"source"` + Referenced bool `json:"referenced"` +} + func Load(path string) (State, error) { data, err := ioutil.ReadFile(path) if os.IsNotExist(err) { From 720e47f4c85a753cb19ef141ce82e9c1c2d0cc18 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Jul 2021 23:06:33 -0400 Subject: [PATCH 4/5] stages/files: write result report to /var/lib/ignition Record some bits of information that the distro might want to know for display purposes. This file is intended to be a reasonably stable interface for distro use. --- internal/distro/distro.go | 7 +- internal/exec/stages/files/files.go | 4 + .../exec/stages/files/filesystemEntries.go | 76 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/internal/distro/distro.go b/internal/distro/distro.go index 9f2a1bcb1..e6865fc13 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -27,8 +27,9 @@ var ( diskByLabelDir = "/dev/disk/by-label" diskByPartUUIDDir = "/dev/disk/by-partuuid" - // File paths + // initrd file paths kernelCmdlinePath = "/proc/cmdline" + bootIDPath = "/proc/sys/kernel/random/boot_id" // initramfs directory containing distro-provided base config systemConfigDir = "/usr/lib/ignition" @@ -74,7 +75,9 @@ var ( // ".ssh/authorized_keys" ("false"). writeAuthorizedKeysFragment = "true" + // Special file paths in the real root luksRealRootKeyFilePath = "/etc/luks/" + resultFilePath = "/var/lib/ignition/result.json" ) func DiskByIDDir() string { return diskByIDDir } @@ -82,6 +85,7 @@ func DiskByLabelDir() string { return diskByLabelDir } func DiskByPartUUIDDir() string { return diskByPartUUIDDir } func KernelCmdlinePath() string { return kernelCmdlinePath } +func BootIDPath() string { return bootIDPath } func SystemConfigDir() string { return fromEnv("SYSTEM_CONFIG_DIR", systemConfigDir) } func GroupaddCmd() string { return groupaddCmd } @@ -113,6 +117,7 @@ func CryptsetupCmd() string { return cryptsetupCmd } func KargsCmd() string { return kargsCmd } func LuksRealRootKeyFilePath() string { return luksRealRootKeyFilePath } +func ResultFilePath() string { return resultFilePath } func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) && !BlackboxTesting() } func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } diff --git a/internal/exec/stages/files/files.go b/internal/exec/stages/files/files.go index a1bff9d79..e08514403 100644 --- a/internal/exec/stages/files/files.go +++ b/internal/exec/stages/files/files.go @@ -87,6 +87,10 @@ func (s stage) Run(config types.Config) error { 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.relabelFiles(); err != nil { return fmt.Errorf("failed to handle relabeling: %v", err) } diff --git a/internal/exec/stages/files/filesystemEntries.go b/internal/exec/stages/files/filesystemEntries.go index 84e4a64bc..5c9eb8e63 100644 --- a/internal/exec/stages/files/filesystemEntries.go +++ b/internal/exec/stages/files/filesystemEntries.go @@ -15,12 +15,15 @@ package files import ( + "encoding/json" "fmt" + "io/ioutil" "os" "os/exec" "path/filepath" "sort" "strings" + "time" cutil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_4_experimental/types" @@ -131,6 +134,79 @@ func (s *stage) createCrypttabEntries(config types.Config) error { return nil } +// createResultFile creates a report recording some details about the +// Ignition run. +func (s *stage) createResultFile() error { + if distro.ResultFilePath() == "" { + return nil + } + + s.Logger.PushPrefix("createResultFile") + defer s.Logger.PopPrefix() + + bootIDBytes, err := ioutil.ReadFile(distro.BootIDPath()) + if err != nil { + return fmt.Errorf("reading boot ID: %w", err) + } + + result := struct { + ProvisioningBootID string `json:"provisioningBootID"` + ProvisioningDate string `json:"provisioningDate"` + UserConfigProvided bool `json:"userConfigProvided"` + }{ + ProvisioningBootID: strings.TrimSpace(string(bootIDBytes)), + ProvisioningDate: time.Now().Format(time.RFC3339), + } + for _, config := range s.State.FetchedConfigs { + if config.Kind == "user" { + result.UserConfigProvided = true + break + } + } + + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Errorf("marshaling result file: %w", err) + } + data = append(data, '\n') + + path, err := s.JoinPath(distro.ResultFilePath()) + if err != nil { + return fmt.Errorf("building result file path: %w", err) + } + contentsUri := dataurl.EncodeBytes(data) + entries := []filesystemEntry{ + // create containing directory with restrictive permissions + dirEntry{ + types.Node{ + Path: filepath.Dir(path), + }, + types.DirectoryEmbedded1{ + Mode: cutil.IntToPtr(0700), + }, + }, + fileEntry{ + types.Node{ + Path: path, + // Ignition is not designed to run twice, + // but don't introduce a hard failure if it + // does + Overwrite: cutil.BoolToPtr(true), + }, + types.FileEmbedded1{ + Contents: types.Resource{ + Source: &contentsUri, + }, + Mode: cutil.IntToPtr(0600), + }, + }, + } + if err := s.createEntries(entries); err != nil { + return fmt.Errorf("adding result file: %v", err) + } + return nil +} + // createFilesystemsEntries creates the files described in config.Storage.{Files,Directories}. func (s *stage) createFilesystemsEntries(config types.Config) error { s.Logger.PushPrefix("createFilesystemsFiles") From 39dab255680c9cdbad30f6e1533c44908dbe7f88 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 21 Jul 2021 17:31:21 -0400 Subject: [PATCH 5/5] stages/files: use IntToPtr() in createCrypttabEntries() --- internal/exec/stages/files/filesystemEntries.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/exec/stages/files/filesystemEntries.go b/internal/exec/stages/files/filesystemEntries.go index 5c9eb8e63..8bcd58974 100644 --- a/internal/exec/stages/files/filesystemEntries.go +++ b/internal/exec/stages/files/filesystemEntries.go @@ -44,7 +44,6 @@ func (s *stage) createCrypttabEntries(config types.Config) error { s.Logger.PushPrefix("createCrypttabEntries") defer s.Logger.PopPrefix() - mode := 0600 path, err := s.JoinPath("/etc/crypttab") if err != nil { return fmt.Errorf("building crypttab filepath: %v", err) @@ -54,7 +53,7 @@ func (s *stage) createCrypttabEntries(config types.Config) error { Path: path, }, types.FileEmbedded1{ - Mode: &mode, + Mode: cutil.IntToPtr(0600), }, } extrafiles := []filesystemEntry{} @@ -89,7 +88,7 @@ func (s *stage) createCrypttabEntries(config types.Config) error { Contents: types.Resource{ Source: &contentsUri, }, - Mode: &mode, + Mode: cutil.IntToPtr(0600), }, }) } @@ -102,7 +101,6 @@ func (s *stage) createCrypttabEntries(config types.Config) error { // already exist) to be mode 0700 rather than auto-creating it at the default directory // permission if len(extrafiles) > 0 { - dirMode := 0700 realpath, err := s.JoinPath(distro.LuksRealRootKeyFilePath()) if err != nil { return fmt.Errorf("building keyfile dir path: %v", err) @@ -117,7 +115,7 @@ func (s *stage) createCrypttabEntries(config types.Config) error { Path: realpath, }, types.DirectoryEmbedded1{ - Mode: &dirMode, + Mode: cutil.IntToPtr(0700), }, }, }, extrafiles...)