From b66fb3ebc76f877cd03ccf678dc8b21f4a8cce37 Mon Sep 17 00:00:00 2001 From: Nikolas Grottendieck Date: Sat, 25 Jun 2022 15:40:46 +0200 Subject: [PATCH 1/4] docs: prepare flatcar & r4e upgrade documentation Sort upgrade documentation alphabetically and move OpenShift after Flatcar. --- docs/upgrading-flatcar.md | 14 ++++++++++++++ docs/upgrading-openshift.md | 2 +- docs/upgrading-r4e.md | 14 ++++++++++++++ docs/upgrading.md | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/upgrading-flatcar.md create mode 100644 docs/upgrading-r4e.md diff --git a/docs/upgrading-flatcar.md b/docs/upgrading-flatcar.md new file mode 100644 index 00000000..5003ab00 --- /dev/null +++ b/docs/upgrading-flatcar.md @@ -0,0 +1,14 @@ +--- +title: Flatcar +parent: Upgrading configs +nav_order: 2 +--- + +# Upgrading Flatcar configs + +Occasionally, changes are made to Flatcar Butane configs (those that specify `variant: flatcar`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} diff --git a/docs/upgrading-openshift.md b/docs/upgrading-openshift.md index 395dd924..f4cca484 100644 --- a/docs/upgrading-openshift.md +++ b/docs/upgrading-openshift.md @@ -1,7 +1,7 @@ --- title: OpenShift parent: Upgrading configs -nav_order: 2 +nav_order: 3 --- # Upgrading OpenShift configs diff --git a/docs/upgrading-r4e.md b/docs/upgrading-r4e.md new file mode 100644 index 00000000..beaedca7 --- /dev/null +++ b/docs/upgrading-r4e.md @@ -0,0 +1,14 @@ +--- +title: RHEL for Edge +parent: Upgrading configs +nav_order: 4 +--- + +# Upgrading RHEL for Edge configs + +Occasionally, changes are made to RHEL for Edge Butane configs (those that specify `variant: r4e`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} diff --git a/docs/upgrading.md b/docs/upgrading.md index c385173c..a364c49b 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -11,4 +11,6 @@ Occasionally, changes are made to Butane configuration specifications that break For details about changes in new versions of Butane config specs, see the guide for your specific config variant: - [Fedora CoreOS](upgrading-fcos.md) (`fcos`) +- [Flatcar](upgrading-flatcar.md) (`flatcar`) - [OpenShift](upgrading-openshift.md) (`openshift`) +- [RHEL for Edge](upgrading-r4e.md) (`r4e`) From 3896e6823eb149b6bf3ec0d6be9a2c3427297766 Mon Sep 17 00:00:00 2001 From: Nikolas Grottendieck Date: Sat, 25 Jun 2022 15:48:50 +0200 Subject: [PATCH 2/4] base/v0_5_exp: add local file embedding for SSH keys (#179) Allow embedding SSH keys via file references by supporting: - multiple file references similar to inline keys - multiple keys per file (one per line) - combinations of inline keys and file embeds --- base/v0_5_exp/schema.go | 29 +- base/v0_5_exp/translate.go | 60 ++++ base/v0_5_exp/translate_test.go | 316 +++++++++++++++++++ config/openshift/v4_13_exp/translate_test.go | 23 +- docs/config-fcos-v1_5-exp.md | 1 + docs/config-flatcar-v1_1-exp.md | 1 + docs/config-openshift-v4_13-exp.md | 1 + docs/config-r4e-v1_1-exp.md | 1 + 8 files changed, 407 insertions(+), 25 deletions(-) diff --git a/base/v0_5_exp/schema.go b/base/v0_5_exp/schema.go index 524b49bb..bf8180ff 100644 --- a/base/v0_5_exp/schema.go +++ b/base/v0_5_exp/schema.go @@ -166,20 +166,21 @@ type PasswdGroup struct { } type PasswdUser struct { - Gecos *string `yaml:"gecos"` - Groups []Group `yaml:"groups"` - HomeDir *string `yaml:"home_dir"` - Name string `yaml:"name"` - NoCreateHome *bool `yaml:"no_create_home"` - NoLogInit *bool `yaml:"no_log_init"` - NoUserGroup *bool `yaml:"no_user_group"` - PasswordHash *string `yaml:"password_hash"` - PrimaryGroup *string `yaml:"primary_group"` - ShouldExist *bool `yaml:"should_exist"` - SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` - Shell *string `yaml:"shell"` - System *bool `yaml:"system"` - UID *int `yaml:"uid"` + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` } type Proxy struct { diff --git a/base/v0_5_exp/translate.go b/base/v0_5_exp/translate.go index d431ee1f..dbc26b7a 100644 --- a/base/v0_5_exp/translate.go +++ b/base/v0_5_exp/translate.go @@ -19,6 +19,7 @@ import ( "os" slashpath "path" "path/filepath" + "regexp" "strings" "text/template" @@ -86,6 +87,7 @@ func (c Config) ToIgn3_4Unvalidated(options common.TranslateOptions) (types.Conf tr.AddCustomTranslator(translateDirectory) tr.AddCustomTranslator(translateLink) tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) @@ -212,6 +214,64 @@ func translateLink(from Link, options common.TranslateOptions) (to types.Link, t return } +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for _, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := readSshKeyFile(options.FilesDir, sshKeyFile) + if err != nil { + r.AddOnError(c, err) + continue + } + + // offset for TranslationSets when both ssh_authorized_keys and ssh_authorized_keys_local are available + offset := len(to.SSHAuthorizedKeys) + for i, line := range regexp.MustCompile("\r?\n").Split(sshKeys, -1) { + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys", i+offset)) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func readSshKeyFile(filesDir string, sshKeyFile string) (string, error) { + // calculate file path within FilesDir and check for path traversal + filePath := filepath.Join(filesDir, sshKeyFile) + if err := baseutil.EnsurePathWithinFilesDir(filePath, filesDir); err != nil { + return "", err + } + contents, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(contents), nil +} + func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { ts := translate.NewTranslationSet("yaml", "json") var r report.Report diff --git a/base/v0_5_exp/translate_test.go b/base/v0_5_exp/translate_test.go index 4508f8e3..4f3ac7b0 100644 --- a/base/v0_5_exp/translate_test.go +++ b/base/v0_5_exp/translate_test.go @@ -1889,6 +1889,322 @@ func TestTranslateTang(t *testing.T) { } } +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + reportSuffix string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid ssh_keys_inline", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys", 0), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + }, + "", + sshKeyDir, + }, + { + "valid ssh_keys_local", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + }, + "", + sshKeyDir, + }, + { + "valid ssh_keys_local with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(""), + types.SSHAuthorizedKey(""), + types.SSHAuthorizedKey(sshKey3), + types.SSHAuthorizedKey(""), + }}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 1), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 2), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 3), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 4), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 5), + }, + }, + "", + sshKeyDir, + }, + { + "valid ssh_keys_local and ssh_keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys", 0), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 1), + }, + }, + "", + sshKeyDir, + }, + { + "valid ssh_keys_local with multiple keys per file and ssh_keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(""), + types.SSHAuthorizedKey(""), + types.SSHAuthorizedKey(sshKey3), + types.SSHAuthorizedKey(""), + }}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys", 0), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 1), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 2), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 3), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 4), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 5), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 6), + }, + }, + "", + sshKeyDir, + }, + { + "valid empty ssh_keys_local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("")}}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + }, + "", + sshKeyDir, + }, + { + "valid blank ssh_keys_local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(""), types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 1), + }, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in ssh_keys_local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(""), + }}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 0), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 1), + }, + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys", 2), + }, + }, + "", + sshKeyDir, + }, + { + "non existing ssh_keys_local file name", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + }, + osNotFound, + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + }, + common.ErrNoFilesDir.Error(), + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + { + From: path.New("yaml", "ssh_authorized_keys_local"), + To: path.New("json", "sshAuthorizedKeys"), + }, + }, + osNotFound, + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + assert.Equal(t, test.out, actual, "translation mismatch") + if len(r.Entries) > 0 { + assert.Truef(t, strings.HasSuffix(r.Entries[0].Message, test.reportSuffix), "report mismatch: expected %q but got %q", test.reportSuffix, r.Entries[0].Message) + } else { + assert.True(t, len(test.reportSuffix) == 0, "unexpected report encountered") + } + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + // TestToIgn3_4 tests the config.ToIgn3_4 function ensuring it will generate a valid config even when empty. Not much else is // tested since it uses the Ignition translation code which has its own set of tests. func TestToIgn3_4(t *testing.T) { diff --git a/config/openshift/v4_13_exp/translate_test.go b/config/openshift/v4_13_exp/translate_test.go index b4b26667..cde91118 100644 --- a/config/openshift/v4_13_exp/translate_test.go +++ b/config/openshift/v4_13_exp/translate_test.go @@ -503,17 +503,18 @@ func TestValidateSupport(t *testing.T) { Groups: []base.Group{ "z", }, - HomeDir: util.StrToPtr("/home/drum"), - NoCreateHome: util.BoolToPtr(true), - NoLogInit: util.BoolToPtr(true), - NoUserGroup: util.BoolToPtr(true), - PasswordHash: util.StrToPtr("corned beef"), - PrimaryGroup: util.StrToPtr("wheel"), - SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, - Shell: util.StrToPtr("/bin/tcsh"), - ShouldExist: util.BoolToPtr(false), - System: util.BoolToPtr(true), - UID: util.IntToPtr(42), + HomeDir: util.StrToPtr("/home/drum"), + NoCreateHome: util.BoolToPtr(true), + NoLogInit: util.BoolToPtr(true), + NoUserGroup: util.BoolToPtr(true), + PasswordHash: util.StrToPtr("corned beef"), + PrimaryGroup: util.StrToPtr("wheel"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + SSHAuthorizedKeysLocal: []string{}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), }, { Name: "bovik", diff --git a/docs/config-fcos-v1_5-exp.md b/docs/config-fcos-v1_5-exp.md index afa098a9..1b7878b3 100644 --- a/docs/config-fcos-v1_5-exp.md +++ b/docs/config-fcos-v1_5-exp.md @@ -181,6 +181,7 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s * **name** (string): the username for the account. * **_password_hash_** (string): the hashed password for the account. * **_ssh_authorized_keys_** (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. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. * **_uid_** (integer): the user ID of the account. * **_gecos_** (string): the GECOS field of the account. * **_home_dir_** (string): the home directory of the account. diff --git a/docs/config-flatcar-v1_1-exp.md b/docs/config-flatcar-v1_1-exp.md index 187d9e8c..860af69b 100644 --- a/docs/config-flatcar-v1_1-exp.md +++ b/docs/config-flatcar-v1_1-exp.md @@ -170,6 +170,7 @@ The Flatcar configuration is a YAML document conforming to the following specifi * **name** (string): the username for the account. * **_password_hash_** (string): the hashed password for the account. * **_ssh_authorized_keys_** (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. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. * **_uid_** (integer): the user ID of the account. * **_gecos_** (string): the GECOS field of the account. * **_home_dir_** (string): the home directory of the account. diff --git a/docs/config-openshift-v4_13-exp.md b/docs/config-openshift-v4_13-exp.md index 6c0a0681..18fb9c0f 100644 --- a/docs/config-openshift-v4_13-exp.md +++ b/docs/config-openshift-v4_13-exp.md @@ -150,6 +150,7 @@ The OpenShift configuration is a YAML document conforming to the following speci * **name** (string): the username for the account. Must be `core`. * **_password_hash_** (string): the hashed password for the account. * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. * **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. diff --git a/docs/config-r4e-v1_1-exp.md b/docs/config-r4e-v1_1-exp.md index 8db11423..a0e2c6ed 100644 --- a/docs/config-r4e-v1_1-exp.md +++ b/docs/config-r4e-v1_1-exp.md @@ -122,6 +122,7 @@ The RHEL for Edge configuration is a YAML document conforming to the following s * **name** (string): the username for the account. * **_password_hash_** (string): the hashed password for the account. * **_ssh_authorized_keys_** (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. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. * **_uid_** (integer): the user ID of the account. * **_gecos_** (string): the GECOS field of the account. * **_home_dir_** (string): the home directory of the account. From e09f037ed1c5aebc7d33501a2fde6a0a4d4bb869 Mon Sep 17 00:00:00 2001 From: Nikolas Grottendieck Date: Sat, 25 Jun 2022 15:54:57 +0200 Subject: [PATCH 3/4] base/v0_5_exp: add local file embedding for systemd units (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow embedding systemd units via file references. A systemd unit has to be either inlined or loaded from an external file – combinations are not possible. --- base/v0_5_exp/schema.go | 16 +-- base/v0_5_exp/translate.go | 69 +++++++++++ base/v0_5_exp/translate_test.go | 193 +++++++++++++++++++++++++++++ base/v0_5_exp/validate.go | 14 +++ base/v0_5_exp/validate_test.go | 90 ++++++++++++++ config/common/errors.go | 1 + docs/config-fcos-v1_5-exp.md | 6 +- docs/config-flatcar-v1_1-exp.md | 6 +- docs/config-openshift-v4_13-exp.md | 6 +- docs/upgrading-fcos.md | 33 +++++ docs/upgrading-flatcar.md | 33 +++++ docs/upgrading-openshift.md | 35 ++++++ docs/upgrading-r4e.md | 33 +++++ test | 3 +- 14 files changed, 524 insertions(+), 14 deletions(-) diff --git a/base/v0_5_exp/schema.go b/base/v0_5_exp/schema.go index bf8180ff..16483d27 100644 --- a/base/v0_5_exp/schema.go +++ b/base/v0_5_exp/schema.go @@ -54,8 +54,9 @@ type Disk struct { } type Dropin struct { - Contents *string `yaml:"contents"` - Name string `yaml:"name"` + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` } type File struct { @@ -248,11 +249,12 @@ type Tree struct { } type Unit struct { - Contents *string `yaml:"contents"` - Dropins []Dropin `yaml:"dropins"` - Enabled *bool `yaml:"enabled"` - Mask *bool `yaml:"mask"` - Name string `yaml:"name"` + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` } type Verification struct { diff --git a/base/v0_5_exp/translate.go b/base/v0_5_exp/translate.go index dbc26b7a..04272404 100644 --- a/base/v0_5_exp/translate.go +++ b/base/v0_5_exp/translate.go @@ -88,6 +88,7 @@ func (c Config) ToIgn3_4Unvalidated(options common.TranslateOptions) (types.Conf tr.AddCustomTranslator(translateLink) tr.AddCustomTranslator(translateResource) tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) @@ -272,6 +273,74 @@ func readSshKeyFile(filesDir string, sshKeyFile string) (string, error) { return string(contents), nil } +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateUnitDropIn) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + // calculate file path within FilesDir and check for + // path traversal + filePath := filepath.Join(options.FilesDir, *from.ContentsLocal) + if err := baseutil.EnsurePathWithinFilesDir(filePath, options.FilesDir); err != nil { + r.AddOnError(c, err) + return + } + contents, err := os.ReadFile(filePath) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateUnitDropIn(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + tm.AddTranslation(c, path.New("json", "contents")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + // calculate file path within FilesDir and check for + // path traversal + filePath := filepath.Join(options.FilesDir, *from.ContentsLocal) + if err := baseutil.EnsurePathWithinFilesDir(filePath, options.FilesDir); err != nil { + r.AddOnError(c, err) + return + } + contents, err := os.ReadFile(filePath) + if err != nil { + r.AddOnError(c, err) + return + } + stringContents := string(contents) + to.Contents = util.StrToPtr(stringContents) + } + + return +} + func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { ts := translate.NewTranslationSet("yaml", "json") var r report.Report diff --git a/base/v0_5_exp/translate_test.go b/base/v0_5_exp/translate_test.go index 4f3ac7b0..554e3f2e 100644 --- a/base/v0_5_exp/translate_test.go +++ b/base/v0_5_exp/translate_test.go @@ -2205,6 +2205,199 @@ func TestTranslateSSHAuthorizedKey(t *testing.T) { } } +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + reportSuffix string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "contents_local"), + To: path.New("json", "contents"), + }, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + osNotFound, + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "contents_local"), + To: path.New("json", "contents"), + }, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + common.ErrNoFilesDir.Error(), + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + osNotFound, + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin_contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "dropins", 0, "contents_local"), + To: path.New("json", "dropins", 0, "contents"), + }, + }, + "", + unitDir, + }, + { + "non existing dropin_contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "dropins", 0, "contents_local"), + To: path.New("json", "dropins", 0, "contents"), + }, + }, + osNotFound, + unitDir, + }, + { + "valid empty dropin_contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "dropins", 0, "contents_local"), + To: path.New("json", "dropins", 0, "contents"), + }, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "dropins", 0, "contents_local"), + To: path.New("json", "dropins", 0, "contents"), + }, + }, + common.ErrNoFilesDir.Error(), + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{ + { + From: path.New("yaml", "dropins", 0, "contents_local"), + To: path.New("json", "dropins", 0, "contents"), + }, + }, + osNotFound, + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + assert.Equal(t, test.out, actual, "translation mismatch") + if len(r.Entries) > 0 { + assert.Truef(t, strings.HasSuffix(r.Entries[0].Message, test.reportSuffix), "report mismatch: expected %q but got %q", test.reportSuffix, r.Entries[0].Message) + } else { + assert.True(t, len(test.reportSuffix) == 0, "unexpected report encountered") + } + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + // TestToIgn3_4 tests the config.ToIgn3_4 function ensuring it will generate a valid config even when empty. Not much else is // tested since it uses the Ignition translation code which has its own set of tests. func TestToIgn3_4(t *testing.T) { diff --git a/base/v0_5_exp/validate.go b/base/v0_5_exp/validate.go index 715a1bb4..0b8332ac 100644 --- a/base/v0_5_exp/validate.go +++ b/base/v0_5_exp/validate.go @@ -76,3 +76,17 @@ func (t Tree) Validate(c path.ContextPath) (r report.Report) { } return } + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("inline"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("inline"), common.ErrTooManySystemdSources) + } + return +} diff --git a/base/v0_5_exp/validate_test.go b/base/v0_5_exp/validate_test.go index 4be185f0..683516fb 100644 --- a/base/v0_5_exp/validate_test.go +++ b/base/v0_5_exp/validate_test.go @@ -289,3 +289,93 @@ func TestValidateFilesystem(t *testing.T) { }) } } + +// TestValidateUnit tests that multiple sources (i.e. local and inline) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // inline specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // inline + local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "inline"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. local and inline) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // inline specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // inline + local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "inline"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} diff --git a/config/common/errors.go b/config/common/errors.go index 5e498235..704e7e70 100644 --- a/config/common/errors.go +++ b/config/common/errors.go @@ -32,6 +32,7 @@ var ( // resources and trees ErrTooManyResourceSources = errors.New("only one of the following can be set: inline, local, source") + ErrTooManySystemdSources = errors.New("only one of the following can be set: inline, local") ErrFilesDirEscape = errors.New("local file path traverses outside the files directory") ErrFileType = errors.New("trees may only contain files, directories, and symlinks") ErrNodeExists = errors.New("matching filesystem node has existing contents or different type") diff --git a/docs/config-fcos-v1_5-exp.md b/docs/config-fcos-v1_5-exp.md index 1b7878b3..43ce960e 100644 --- a/docs/config-fcos-v1_5-exp.md +++ b/docs/config-fcos-v1_5-exp.md @@ -172,10 +172,12 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. - * **_contents_** (string): the contents of the unit. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. * **name** (string): the name of the drop-in. This must be suffixed with ".conf". - * **_contents_** (string): the contents of the drop-in. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/config-flatcar-v1_1-exp.md b/docs/config-flatcar-v1_1-exp.md index 860af69b..43c307aa 100644 --- a/docs/config-flatcar-v1_1-exp.md +++ b/docs/config-flatcar-v1_1-exp.md @@ -161,10 +161,12 @@ The Flatcar configuration is a YAML document conforming to the following specifi * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. - * **_contents_** (string): the contents of the unit. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. * **name** (string): the name of the drop-in. This must be suffixed with ".conf". - * **_contents_** (string): the contents of the drop-in. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/config-openshift-v4_13-exp.md b/docs/config-openshift-v4_13-exp.md index 18fb9c0f..8a365877 100644 --- a/docs/config-openshift-v4_13-exp.md +++ b/docs/config-openshift-v4_13-exp.md @@ -141,10 +141,12 @@ The OpenShift configuration is a YAML document conforming to the following speci * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. - * **_contents_** (string): the contents of the unit. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. * **name** (string): the name of the drop-in. This must be suffixed with ".conf". - * **_contents_** (string): the contents of the drop-in. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. Must be `core`. diff --git a/docs/upgrading-fcos.md b/docs/upgrading-fcos.md index f4c5e882..8ef49ac2 100644 --- a/docs/upgrading-fcos.md +++ b/docs/upgrading-fcos.md @@ -13,6 +13,39 @@ Occasionally, changes are made to Fedora CoreOS Butane configs (those that speci 1. TOC {:toc} +{% comment %} + +## From Version 1.4.0 to Version 1.5.0 + +There are no breaking changes between versions 1.4.0 and 1.5.0 of the `fcos` configuration specification. Any valid 1.4.0 configuration can be updated to a 1.5.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: fcos +version: 1.5.0-experimental +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +{% endcomment %} + ## From Version 1.3.0 to 1.4.0 There are no breaking changes between versions 1.3.0 and 1.4.0 of the `fcos` configuration specification. Any valid 1.3.0 configuration can be updated to a 1.4.0 configuration by changing the version string in the config. diff --git a/docs/upgrading-flatcar.md b/docs/upgrading-flatcar.md index 5003ab00..ae341d9c 100644 --- a/docs/upgrading-flatcar.md +++ b/docs/upgrading-flatcar.md @@ -12,3 +12,36 @@ Occasionally, changes are made to Flatcar Butane configs (those that specify `va 1. TOC {:toc} + +{% comment %} + +## From Version 1.0.0 to Version 1.1.0 + +There are no breaking changes between versions 1.0.0 and 1.1.0 of the `flatcar` configuration specification. Any valid 1.0.0 configuration can be updated to a 1.1.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: flatcar +version: 1.1.0-experimental +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +{% endcomment %} diff --git a/docs/upgrading-openshift.md b/docs/upgrading-openshift.md index f4cca484..47976834 100644 --- a/docs/upgrading-openshift.md +++ b/docs/upgrading-openshift.md @@ -13,6 +13,41 @@ Occasionally, changes are made to OpenShift Butane configs (those that specify ` 1. TOC {:toc} +{% comment %} + +## From Version 4.12.0 to 4.13.0 + +There are no breaking changes between versions 4.12.0 and 4.13.0 of the `openshift` configuration specification. Any valid 4.12.0 configuration can be updated to a 4.13.0 configuration by changing the version string in the config. + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: openshift +version: 4.13.0-experimental +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.service +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +{% endcomment %} + ## From Version 4.11.0 to 4.12.0 There are no breaking changes between versions 4.11.0 and 4.12.0 of the `openshift` configuration specification. Any valid 4.11.0 configuration can be updated to a 4.12.0 configuration by changing the version string in the config. diff --git a/docs/upgrading-r4e.md b/docs/upgrading-r4e.md index beaedca7..07873893 100644 --- a/docs/upgrading-r4e.md +++ b/docs/upgrading-r4e.md @@ -12,3 +12,36 @@ Occasionally, changes are made to RHEL for Edge Butane configs (those that speci 1. TOC {:toc} + +{% comment %} + +## From Version 1.0.0 to Version 1.1.0 + +There are no breaking changes between versions 1.0.0 and 1.1.0 of the `r4e` configuration specification. Any valid 1.0.0 configuration can be updated to a 1.1.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: r4e +version: 1.1.0-experimental +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +{% endcomment %} diff --git a/test b/test index 69f55966..c6d57139 100755 --- a/test +++ b/test @@ -50,7 +50,8 @@ if [ -n "${csplit}" ] && [ -n "${head}" ]; then trap 'rm -r tmpdocs' EXIT # Create files-dir contents expected by configs mkdir -p tmpdocs/files-dir/tree - touch tmpdocs/files-dir/{config.ign,ca.pem,file,file-epilogue,local-file3} + touch tmpdocs/files-dir/{config.ign,ca.pem,example.conf,example.service,file,file-epilogue,local-file3} + echo "ssh-rsa AAAA" > tmpdocs/files-dir/id_rsa.pub for doc in docs/*md do From c6e10cd3314baf53733aaa3f9379cc321a8b3635 Mon Sep 17 00:00:00 2001 From: Nikolas Grottendieck Date: Mon, 12 Sep 2022 21:05:58 +0200 Subject: [PATCH 4/4] docs: update release notes for local file embedding feature Resolves #179 --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9438fe3a..0f1a96d8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,7 @@ nav_order: 9 flatcar 1.1.0-exp, openshift 4.13.0-exp)_ - Allow specifying user password hash _(openshift 4.13.0-exp)_ - Support offline Tang provisioning via pre-shared advertisement _(fcos 1.5.0-exp, openshift 4.13.0-exp)_ +- Support local file embedding for SSH keys and systemd units ### Bug fixes