diff --git a/doc/configuration-v3_0-experimental.md b/doc/configuration-v3_0-experimental.md index 7f35ab041..199b699b0 100644 --- a/doc/configuration-v3_0-experimental.md +++ b/doc/configuration-v3_0-experimental.md @@ -104,7 +104,7 @@ The Ignition configuration is a JSON document conforming to the following specif * **_users_** (list of objects): the list of accounts that shall exist. * **name** (string): the username for the account. * **_passwordHash_** (string): the encrypted password for the account. - * **_sshAuthorizedKeys_** (list of strings): a list of SSH keys to be added to the user's authorized_keys. + * **_sshAuthorizedKeys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. * **_uid_** (integer): the user ID of the account. * **_gecos_** (string): the GECOS field of the account. * **_homeDir_** (string): the home directory of the account. diff --git a/internal/authorized_keys_d/as_user/as_user.c b/internal/as_user/as_user.c similarity index 100% rename from internal/authorized_keys_d/as_user/as_user.c rename to internal/as_user/as_user.c diff --git a/internal/authorized_keys_d/as_user/as_user.go b/internal/as_user/as_user.go similarity index 100% rename from internal/authorized_keys_d/as_user/as_user.go rename to internal/as_user/as_user.go diff --git a/internal/authorized_keys_d/as_user/as_user.h b/internal/as_user/as_user.h similarity index 100% rename from internal/authorized_keys_d/as_user/as_user.h rename to internal/as_user/as_user.h diff --git a/internal/authorized_keys_d/authorized_keys_d.go b/internal/authorized_keys_d/authorized_keys_d.go deleted file mode 100644 index a5e3e7f2b..000000000 --- a/internal/authorized_keys_d/authorized_keys_d.go +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright 2015 CoreOS, 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. - -// +build linux - -// authorized_keys_d manages a user's ~/.ssh/authorized_keys.d and can produce -// a ~/.ssh/authorized_keys file from the authorized_keys.d contents. -package authorized_keys_d - -import ( - "fmt" - "io/ioutil" - "os" - "os/user" - "path/filepath" - "sort" - "strings" - "syscall" - - "github.com/coreos/ignition/internal/authorized_keys_d/as_user" -) - -const ( - AuthorizedKeysFile = "authorized_keys" - AuthorizedKeysDir = "authorized_keys.d" - PreservedKeysName = "orig_authorized_keys" - SSHDir = ".ssh" - - lockFile = ".authorized_keys.d.lock" // In "~/". - stageFile = ".authorized_keys.d.stage_file" // In "~/.ssh". - stageDir = ".authorized_keys.d.stage_dir" // In "~/.ssh". -) - -// SSHAuthorizedKeysDir represents an opened user's authorized_keys.d. -type SSHAuthorizedKeysDir struct { - path string // Path to authorized_keys.d directory. - user *user.User // User of the directory. - lock *os.File // Lock file for serializing Open()-Close(). -} - -// SSHAuthorizedKey represents an opened user's authorized_keys.d/ entry. -type SSHAuthorizedKey struct { - Name string // Name given to the key. - Disabled bool // Disabled state of the key. - Path string // Path to the file backing the key. - origin *SSHAuthorizedKeysDir // Originating dir for this key. -} - -// sshDirPath returns the path to the .ssh dir for the user. -func sshDirPath(u *user.User) string { - return filepath.Join(u.HomeDir, SSHDir) -} - -// authKeysFilePath returns the path to the authorized_keys file for the user. -func authKeysFilePath(u *user.User) string { - return filepath.Join(sshDirPath(u), AuthorizedKeysFile) -} - -// authKeysDirPath returns the path to the authorized_keys.d for the user. -func authKeysDirPath(u *user.User) string { - return filepath.Join(sshDirPath(u), AuthorizedKeysDir) -} - -// lockFilePath returns the path to the lock file for the user. -func lockFilePath(u *user.User) string { - return filepath.Join(u.HomeDir, lockFile) -} - -// stageDirPath returns the path to the staging directory for the user. -func stageDirPath(u *user.User) string { - return filepath.Join(sshDirPath(u), stageDir) -} - -// stageFilePath returns the path to the staging file for the user. -func stageFilePath(u *user.User) string { - return filepath.Join(sshDirPath(u), stageFile) -} - -// opendir opens the authorized keys directory. -func opendir(dir string) (*SSHAuthorizedKeysDir, error) { - fi, err := os.Stat(dir) - if err != nil { - return nil, err - } - if !fi.IsDir() { - return nil, fmt.Errorf("%q is not a directory", dir) - } - return &SSHAuthorizedKeysDir{path: dir}, nil -} - -// acquireLock locks the lock file for the given user's authorized_keys.d. -// A lock file is created if it doesn't already exist. -// The locking is currently a simple coarse-grained mutex held for the -// Open()-Close() duration, implemented using a lock file in the user's ~/. -func acquireLock(u *user.User) (*os.File, error) { - f, err := as_user.OpenFile(u, lockFilePath(u), - syscall.O_CREAT|syscall.O_RDONLY, 0600) - if err != nil { - return nil, err - } - if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { - f.Close() - return nil, err - } - return f, nil -} - -// createAuthorizedKeysDir creates an authorized keys directory for the user. -// If the user has an authorized_keys file, it is migrated. -func createAuthorizedKeysDir(u *user.User) (*SSHAuthorizedKeysDir, error) { - td := stageDirPath(u) - if err := as_user.MkdirAll(u, td, 0700); err != nil { - return nil, err - } - defer os.RemoveAll(td) - - akd, err := opendir(td) - if err != nil { - return nil, err - } - akd.user = u - - akfb, err := ioutil.ReadFile(authKeysFilePath(u)) - if err != nil && !os.IsNotExist(err) { - return nil, err - } else if err == nil { - err = akd.Add(PreservedKeysName, akfb, false, false) - if err != nil { - return nil, err - } - } - if err = akd.rename(authKeysDirPath(u)); err != nil { - return nil, err - } - return akd, err -} - -// Open opens the authorized keys directory for the supplied user. -// If create is false, Open will fail if no directory exists yet. -// If create is true, Open will create the directory if it doesn't exist, -// preserving the authorized_keys file in the process. -// After a successful open, Close should be called when finished to unlock -// the directory. -func Open(usr *user.User, create bool) (*SSHAuthorizedKeysDir, error) { - l, err := acquireLock(usr) - if err != nil { - return nil, err - } - defer func() { - if err != nil { - l.Close() - } - }() - - akd, err := opendir(authKeysDirPath(usr)) - if err != nil && (!create || !os.IsNotExist(err)) { - return nil, err - } else if os.IsNotExist(err) { - akd, err = createAuthorizedKeysDir(usr) - if err != nil { - return nil, err - } - } - - akd.lock = l - akd.user = usr - return akd, nil -} - -// Close closes the authorized keys directory. -func (akd *SSHAuthorizedKeysDir) Close() error { - return akd.lock.Close() -} - -// rename renames the authorized_keys dir to the supplied path. -func (akd *SSHAuthorizedKeysDir) rename(to string) error { - err := as_user.Rename(akd.user, akd.path, to) - if err != nil { - return err - } - akd.path = to - return nil -} - -// keyPath returns the path to the named key. -func (akd *SSHAuthorizedKeysDir) keyPath(n string) string { - return filepath.Join(akd.path, n) -} - -// WalkKeys iterates across all keys in akd, calling f for each key. -// Iterating stops on error, and the error is propagated out. -func (akd *SSHAuthorizedKeysDir) WalkKeys(f func(*SSHAuthorizedKey) error) error { - d, err := os.Open(akd.path) - if err != nil { - return err - } - - names, err := d.Readdirnames(0) - if err != nil { - return err - } - - sort.Strings(names) - for _, n := range names { - ak, err := akd.Open(n) - if err != nil { - return err - } - if err := f(ak); err != nil { - return err - } - } - - return nil -} - -// Open opens the key at name. -func (akd *SSHAuthorizedKeysDir) Open(name string) (*SSHAuthorizedKey, error) { - p := akd.keyPath(name) - fi, err := os.Stat(p) - if err != nil { - return nil, err - } - ak := &SSHAuthorizedKey{ - Name: name, - Disabled: (fi.Size() == 0), - Path: p, - origin: akd, - } - return ak, nil -} - -// Remove removes the key at name. -func (akd *SSHAuthorizedKeysDir) Remove(name string) error { - ak, err := akd.Open(name) - if err != nil { - return err - } - return ak.Remove() -} - -// Disable disables the key at name. -func (akd *SSHAuthorizedKeysDir) Disable(name string) error { - ak, err := akd.Open(name) - if err != nil { - return err - } - return ak.Disable() -} - -// Add adds the supplied key at name. -// replace enables replacing keys already existing at name. -// force enables adding keys to a disabled name, enabling it in the process. -// Names starting wtih ".", and anything containing "/" are disallowed. -func (akd *SSHAuthorizedKeysDir) Add(name string, keys []byte, replace, force bool) error { - if strings.HasPrefix(name, ".") || strings.Contains(name, "/") { - return fmt.Errorf(`illegal name`) - } - - p := akd.keyPath(name) - fi, err := os.Stat(p) - if err == nil { - if fi.Size() > 0 && !replace { - return fmt.Errorf("key %q already exists", name) - } else if fi.Size() == 0 && !force { - return fmt.Errorf("key %q disabled", name) - } - } else if !os.IsNotExist(err) { - return err - } - ak := &SSHAuthorizedKey{Path: p, origin: akd} - return ak.Replace(keys) -} - -// KeysFilePath returns the backing authorized_keys file path for this -// SSHAuthorizedKeysDir. This is the file written to by Sync(). -func (akd *SSHAuthorizedKeysDir) KeysFilePath() string { - return authKeysFilePath(akd.user) -} - -// KeysDirPath returns the authorized_keys.d directory path for this -// SSHAuthorizedKeysDir. This is the directory containing the discrete key -// files. -func (akd *SSHAuthorizedKeysDir) KeysDirPath() string { - return authKeysDirPath(akd.user) -} - -// Sync synchronizes the user's ~/.ssh/authorized_keys file with the -// current authorized_keys.d directory state. -func (akd *SSHAuthorizedKeysDir) Sync() error { - sp := stageFilePath(akd.user) - sf, err := as_user.OpenFile(akd.user, sp, - syscall.O_CREAT|syscall.O_TRUNC|syscall.O_WRONLY, 0600) - if err != nil { - return err - } - defer func() { - if err != nil { - sf.Close() - os.Remove(sp) - } - }() - - if err := akd.WalkKeys(func(k *SSHAuthorizedKey) error { - if !k.Disabled { - kb, err := ioutil.ReadFile(k.Path) - if err != nil { - return err - } - kb = append(kb, '\n') - if _, err := sf.Write(kb); err != nil { - return err - } - } - return nil - }); err != nil { - return err - } - - if err := sf.Close(); err != nil { - return err - } - - err = as_user.Rename(akd.user, sp, authKeysFilePath(akd.user)) - if err != nil { - return err - } - - return nil -} - -// Remove removes the opened key. -func (ak *SSHAuthorizedKey) Remove() error { - return os.Remove(ak.Path) -} - -// Disable disables the opened key. -func (ak *SSHAuthorizedKey) Disable() error { - return os.Truncate(ak.Path, 0) -} - -// Replace replaces the opened key with the supplied data. -func (ak *SSHAuthorizedKey) Replace(keys []byte) error { - sp := stageFilePath(ak.origin.user) - sf, err := as_user.OpenFile(ak.origin.user, sp, - syscall.O_WRONLY|syscall.O_CREAT|syscall.O_TRUNC, 0600) - if err != nil { - return err - } - defer os.Remove(sp) - if _, err = sf.Write(keys); err != nil { - return err - } - if err := sf.Close(); err != nil { - return err - } - return as_user.Rename(ak.origin.user, sp, ak.Path) -} diff --git a/internal/distro/distro.go b/internal/distro/distro.go index 99f3b0e84..2f5eb5b56 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -57,6 +57,11 @@ var ( // Flags selinuxRelabel = "false" blackboxTesting = "false" + // writeAuthorizedKeysFragment indicates whether to write SSH keys + // specified in the Ignition config as a fragment to + // ".ssh/authorized_keys.d/ignition" ("true"), or to + // ".ssh/authorized_keys" ("false"). + writeAuthorizedKeysFragment = "true" ) func DiskByIDDir() string { return diskByIDDir } @@ -85,6 +90,9 @@ func XfsMkfsCmd() string { return xfsMkfsCmd } func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) } func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } +func WriteAuthorizedKeysFragment() bool { + return bakedStringToBool(fromEnv("WRITE_AUTHORIZED_KEYS_FRAGMENT", writeAuthorizedKeysFragment)) +} func fromEnv(nameSuffix, defaultValue string) string { value := os.Getenv("IGNITION_" + nameSuffix) diff --git a/internal/exec/util/passwd.go b/internal/exec/util/passwd.go index 6837e4448..b5e7a44e2 100644 --- a/internal/exec/util/passwd.go +++ b/internal/exec/util/passwd.go @@ -17,11 +17,13 @@ package util import ( "fmt" "os/exec" + "os/user" + "path/filepath" "strconv" "strings" "syscall" - keys "github.com/coreos/ignition/internal/authorized_keys_d" + "github.com/coreos/ignition/internal/as_user" "github.com/coreos/ignition/internal/config/types" "github.com/coreos/ignition/internal/distro" "github.com/coreos/ignition/internal/log" @@ -160,6 +162,26 @@ func translateV2_1PasswdUserGroupSliceToStringSlice(groups []types.Group) []stri return newGroups } +// writeAuthKeysFile writes the content in keys to the path fp for the user, +// creating any directories in fp as needed. +func writeAuthKeysFile(u *user.User, fp string, keys []byte) error { + if err := as_user.MkdirAll(u, filepath.Dir(fp), 0700); err != nil { + return err + } + + f, err := as_user.OpenFile(u, fp, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_TRUNC, 0600) + if err != nil { + return err + } + if _, err = f.Write(keys); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + return nil +} + // AuthorizeSSHKeys adds the provided SSH public keys to the user's authorized keys. func (u Util) AuthorizeSSHKeys(c types.PasswdUser) error { if len(c.SSHAuthorizedKeys) == 0 { @@ -172,12 +194,6 @@ func (u Util) AuthorizeSSHKeys(c types.PasswdUser) error { return fmt.Errorf("unable to lookup user %q", c.Name) } - akd, err := keys.Open(usr, true) - if err != nil { - return err - } - defer akd.Close() - // TODO(vc): introduce key names to config? // TODO(vc): validate c.SSHAuthorizedKeys well-formedness. ks := strings.Join(translateV2_1SSHAuthorizedKeySliceToStringSlice(c.SSHAuthorizedKeys), "\n") @@ -189,12 +205,10 @@ func (u Util) AuthorizeSSHKeys(c types.PasswdUser) error { ks = ks + "\n" } - if err := akd.Add("ignition", []byte(ks), true, true); err != nil { - return err - } - - if err := akd.Sync(); err != nil { - return err + if distro.WriteAuthorizedKeysFragment() { + writeAuthKeysFile(usr, filepath.Join(usr.HomeDir, ".ssh", "authorized_keys.d", "ignition"), []byte(ks)) + } else { + writeAuthKeysFile(usr, filepath.Join(usr.HomeDir, ".ssh", "authorized_keys"), []byte(ks)) } return nil diff --git a/tests/blackbox_test.go b/tests/blackbox_test.go index 937c15fe4..6926497c7 100644 --- a/tests/blackbox_test.go +++ b/tests/blackbox_test.go @@ -272,9 +272,8 @@ func outer(t *testing.T, test types.Test, negativeTests bool) error { } // Ignition - appendEnv := []string{ - "IGNITION_SYSTEM_CONFIG_DIR=" + systemConfigDir, - } + appendEnv := test.Env + appendEnv = append(appendEnv, "IGNITION_SYSTEM_CONFIG_DIR="+systemConfigDir) if !negativeTests { if err := runIgnition(t, ctx, "disks", "", tmpDirectory, appendEnv); err != nil { diff --git a/tests/positive/passwd/users.go b/tests/positive/passwd/users.go index 0b4abf07a..22d2362fa 100644 --- a/tests/positive/passwd/users.go +++ b/tests/positive/passwd/users.go @@ -21,12 +21,14 @@ import ( func init() { register.Register(register.PositiveTest, AddPasswdUsers()) + register.Register(register.PositiveTest, UseAuthorizedKeysFile()) } func AddPasswdUsers() types.Test { name := "Adding users" in := types.GetBaseDisk() out := types.GetBaseDisk() + env := []string{"IGNITION_WRITE_AUTHORIZED_KEYS_FRAGMENT=true"} config := `{ "ignition": { "version": "$version" @@ -140,6 +142,82 @@ ENCRYPT_METHOD SHA512 }, Contents: "root:*::root\nusers:*::\nsudo:*::\nwheel:*::root,core\nsudo:*::\ndocker:*::core\nsystemd-coredump:!!::\nfleet:!!::core\nrkt-admin:!!::\nrkt:!!::core\ncore:*::\ntest:!::\njenkins:!::\n", }, + { + Node: types.Node{ + Name: "ignition", + Directory: "home/test/.ssh/authorized_keys.d", + User: 1000, + Group: 1000, + }, + Contents: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBRZPFJNOvQRfokigTtl0IBi71LHZrFOk4EJ3Zowtk/bX5uIVai0Cd4+hqlocYL10idgtFBH28skeKfsmHwgS9XwOvP+g+kqAl7yCz8JEzIUzl1fxNZDToi0jA3B5MwXkpt+IWfnabwi2cRZhlzrz9rO+eExu5s3NfaRmmmCYrjCJIRPKSCrW8U0n9fVSbX4PDdMXVmH7r+t8MtR8523vCbakFR/Y0YIqkPVdfuUXHh9rDCdH4B7mt7nYX2LWQXGUvmI13mgQoy04ifkaR3ImuOMp3Y1J1gm6clO74IMCq/sK9+XJhbxMPPHUoUJ2EwbaG7Dbh3iqz47e9oVki4gIH stephenlowrie@localhost.localdomain\n", + }, + }) + + return types.Test{ + Name: name, + In: in, + Out: out, + Env: env, + Config: config, + ConfigMinVersion: configMinVersion, + } +} + +// UseAuthorizedKeysFile verifies that ~/.ssh/authorized_keys is written +// when IGNITION_WRITE_AUTHORIZED_KEYS_FRAGMENT=false. +func UseAuthorizedKeysFile() types.Test { + name := "Use authorized_keys file" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + env := []string{"IGNITION_WRITE_AUTHORIZED_KEYS_FRAGMENT=false"} + config := `{ + "ignition": { + "version": "$version" + }, + "passwd": { + "users": [{ + "name": "test", + "create": {}, + "passwordHash": "zJW/EKqqIk44o", + "sshAuthorizedKeys": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBRZPFJNOvQRfokigTtl0IBi71LHZrFOk4EJ3Zowtk/bX5uIVai0Cd4+hqlocYL10idgtFBH28skeKfsmHwgS9XwOvP+g+kqAl7yCz8JEzIUzl1fxNZDToi0jA3B5MwXkpt+IWfnabwi2cRZhlzrz9rO+eExu5s3NfaRmmmCYrjCJIRPKSCrW8U0n9fVSbX4PDdMXVmH7r+t8MtR8523vCbakFR/Y0YIqkPVdfuUXHh9rDCdH4B7mt7nYX2LWQXGUvmI13mgQoy04ifkaR3ImuOMp3Y1J1gm6clO74IMCq/sK9+XJhbxMPPHUoUJ2EwbaG7Dbh3iqz47e9oVki4gIH stephenlowrie@localhost.localdomain" + ] + } + ] + } + }` + configMinVersion := "3.0.0-experimental" + in[0].Partitions.AddFiles("ROOT", []types.File{ + { + Node: types.Node{ + Name: "passwd", + Directory: "etc", + }, + Contents: "root:x:0:0:root:/root:/bin/bash\ncore:x:500:500:CoreOS Admin:/home/core:/bin/bash\nsystemd-coredump:x:998:998:systemd Core Dumper:/:/sbin/nologin\nfleet:x:253:253::/:/sbin/nologin\n", + }, + { + Node: types.Node{ + Name: "shadow", + Directory: "etc", + }, + Contents: "root:*:15887:0:::::\ncore:*:15887:0:::::\nsystemd-coredump:!!:17301::::::\nfleet:!!:17301::::::\n", + }, + { + Node: types.Node{ + Name: "group", + Directory: "etc", + }, + Contents: "root:x:0:root\nwheel:x:10:root,core\nsudo:x:150:\ndocker:x:233:core\nsystemd-coredump:x:998:\nfleet:x:253:core\ncore:x:500:\nrkt-admin:x:999:\nrkt:x:251:core\n", + }, + { + Node: types.Node{ + Name: "gshadow", + Directory: "etc", + }, + Contents: "root:*::root\nusers:*::\nsudo:*::\nwheel:*::root,core\nsudo:*::\ndocker:*::core\nsystemd-coredump:!!::\nfleet:!!::core\nrkt-admin:!!::\nrkt:!!::core\ncore:*::\n", + }, + }) + out[0].Partitions.AddFiles("ROOT", []types.File{ { Node: types.Node{ Name: "authorized_keys", @@ -147,7 +225,7 @@ ENCRYPT_METHOD SHA512 User: 1000, Group: 1000, }, - Contents: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBRZPFJNOvQRfokigTtl0IBi71LHZrFOk4EJ3Zowtk/bX5uIVai0Cd4+hqlocYL10idgtFBH28skeKfsmHwgS9XwOvP+g+kqAl7yCz8JEzIUzl1fxNZDToi0jA3B5MwXkpt+IWfnabwi2cRZhlzrz9rO+eExu5s3NfaRmmmCYrjCJIRPKSCrW8U0n9fVSbX4PDdMXVmH7r+t8MtR8523vCbakFR/Y0YIqkPVdfuUXHh9rDCdH4B7mt7nYX2LWQXGUvmI13mgQoy04ifkaR3ImuOMp3Y1J1gm6clO74IMCq/sK9+XJhbxMPPHUoUJ2EwbaG7Dbh3iqz47e9oVki4gIH stephenlowrie@localhost.localdomain\n\n", + Contents: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBRZPFJNOvQRfokigTtl0IBi71LHZrFOk4EJ3Zowtk/bX5uIVai0Cd4+hqlocYL10idgtFBH28skeKfsmHwgS9XwOvP+g+kqAl7yCz8JEzIUzl1fxNZDToi0jA3B5MwXkpt+IWfnabwi2cRZhlzrz9rO+eExu5s3NfaRmmmCYrjCJIRPKSCrW8U0n9fVSbX4PDdMXVmH7r+t8MtR8523vCbakFR/Y0YIqkPVdfuUXHh9rDCdH4B7mt7nYX2LWQXGUvmI13mgQoy04ifkaR3ImuOMp3Y1J1gm6clO74IMCq/sK9+XJhbxMPPHUoUJ2EwbaG7Dbh3iqz47e9oVki4gIH stephenlowrie@localhost.localdomain\n", }, }) @@ -155,6 +233,7 @@ ENCRYPT_METHOD SHA512 Name: name, In: in, Out: out, + Env: env, Config: config, ConfigMinVersion: configMinVersion, } diff --git a/tests/types/types.go b/tests/types/types.go index 9d9bfacb2..4dafaae5a 100644 --- a/tests/types/types.go +++ b/tests/types/types.go @@ -96,6 +96,7 @@ type Test struct { Out []Disk // Expected disk state after running Ignition MntDevices []MntDevice SystemDirFiles []File + Env []string // Environment variables for Ignition Config string ConfigMinVersion string ConfigVersion string