diff --git a/pkg/customizations/kickstart/kickstart.go b/pkg/customizations/kickstart/kickstart.go index 8d3842a10e..18574506f5 100644 --- a/pkg/customizations/kickstart/kickstart.go +++ b/pkg/customizations/kickstart/kickstart.go @@ -2,6 +2,7 @@ package kickstart import ( "fmt" + "strings" "github.com/osbuild/blueprint/pkg/blueprint" "github.com/osbuild/images/pkg/customizations/users" @@ -89,5 +90,40 @@ func (options Options) Validate() error { return fmt.Errorf("kickstart users and/or groups are not compatible with user-supplied kickstart content") } } + + // This check repeats the same checks that are made in the kickstart stage + // constructor. Repeating it here catches it earlier in the pipeline + // generation, before serialization starts. + for _, user := range options.Users { + if user.Name == "root" { + // return an error if any other field is set (except SSH) + unsupportedOptionsSet := make([]string, 0, 7) + if user.ExpireDate != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "expiredate") + } + if user.ForcePasswordReset != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "force_password_reset") + } + if user.GID != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "gid") + } + if user.Groups != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "groups") + } + if user.Home != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "home") + } + if user.Shell != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "shell") + } + if user.UID != nil { + unsupportedOptionsSet = append(unsupportedOptionsSet, "uid") + } + if len(unsupportedOptionsSet) > 0 { + return fmt.Errorf("unsupported kickstart options for user \"root\": %s", strings.Join(unsupportedOptionsSet, ", ")) + } + } + } + return nil } diff --git a/pkg/customizations/kickstart/kickstart_test.go b/pkg/customizations/kickstart/kickstart_test.go new file mode 100644 index 0000000000..158729a381 --- /dev/null +++ b/pkg/customizations/kickstart/kickstart_test.go @@ -0,0 +1,159 @@ +package kickstart_test + +import ( + "testing" + + "github.com/osbuild/blueprint/pkg/blueprint" + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/customizations/users" + "github.com/stretchr/testify/assert" +) + +func TestNewKickstart(t *testing.T) { + type testCase struct { + customizations *blueprint.Customizations + + expOptions *kickstart.Options + expErr string + } + + testCases := map[string]testCase{ + "empty": { + expOptions: &kickstart.Options{ + Users: []users.User{}, + Groups: []users.Group{}, + }, + }, + + "users+groups": { + // we don't need to extensively test the whole blueprint.User -> + // users.User and blueprint.Group -> users.Group conversion here, + // so just one test case is enough + customizations: &blueprint.Customizations{ + User: []blueprint.UserCustomization{ + { + Name: "alice", + Key: common.ToPtr("ssh-ed25519 iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"), + Home: common.ToPtr("/var/home/alice"), + }, + { + Name: "bob", + Key: common.ToPtr("ssh-ed25519 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), + Home: common.ToPtr("/var/home/bob"), + }, + }, + Group: []blueprint.GroupCustomization{ + { + Name: "data", + GID: common.ToPtr(2010), + }, + { + Name: "datarw", + GID: common.ToPtr(2020), + }, + }, + }, + + expOptions: &kickstart.Options{ + Users: []users.User{ + { + Name: "alice", + Key: common.ToPtr("ssh-ed25519 iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"), + Home: common.ToPtr("/var/home/alice"), + }, + { + Name: "bob", + Key: common.ToPtr("ssh-ed25519 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), + Home: common.ToPtr("/var/home/bob"), + }, + }, + Groups: []users.Group{ + { + Name: "data", + GID: common.ToPtr(2010), + }, + { + Name: "datarw", + GID: common.ToPtr(2020), + }, + }, + }, + }, + + "installer-customizations-kickstart-contents": { + customizations: &blueprint.Customizations{ + Installer: &blueprint.InstallerCustomization{ + Kickstart: &blueprint.Kickstart{ + Contents: "echo 'Hello!!'", + }, + }, + }, + + expOptions: &kickstart.Options{ + Users: []users.User{}, + Groups: []users.Group{}, + UserFile: &kickstart.File{ + Contents: "echo 'Hello!!'", + }, + }, + }, + + "installer-customizations-unattended": { + customizations: &blueprint.Customizations{ + Installer: &blueprint.InstallerCustomization{ + Unattended: true, + }, + }, + + expOptions: &kickstart.Options{ + Users: []users.User{}, + Groups: []users.Group{}, + Unattended: true, + }, + }, + + "error/installer-customizations-kickstart+unattended": { + customizations: &blueprint.Customizations{ + Installer: &blueprint.InstallerCustomization{ + Kickstart: &blueprint.Kickstart{ + Contents: "echo 'Hello!!'", + }, + Unattended: true, + }, + }, + + expErr: "installer.unattended is not supported when adding custom kickstart contents", + }, + + "error/unsupported-root-options": { + customizations: &blueprint.Customizations{ + User: []blueprint.UserCustomization{ + { + Name: "root", + Home: common.ToPtr("/megaroot"), + Shell: common.ToPtr("/bin/zsh"), + Groups: []string{"cups"}, + }, + }, + }, + + expErr: "unsupported kickstart options for user \"root\": groups, home, shell", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + options, err := kickstart.New(tc.customizations) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expOptions, options) + }) + } +} diff --git a/pkg/manifest/anaconda_installer_iso_tree.go b/pkg/manifest/anaconda_installer_iso_tree.go index 7f1b742f80..63dd5b908b 100644 --- a/pkg/manifest/anaconda_installer_iso_tree.go +++ b/pkg/manifest/anaconda_installer_iso_tree.go @@ -538,7 +538,7 @@ func (p *AnacondaInstallerISOTree) serialize() (osbuild.Pipeline, error) { case p.OSPipeline != nil: tarPayloadStages, err := p.tarPayloadStages() if err != nil { - return osbuild.Pipeline{}, fmt.Errorf("cannot create ostree container stages: %w", err) + return osbuild.Pipeline{}, fmt.Errorf("cannot create tar payload stages: %w", err) } pipeline.AddStages(tarPayloadStages...) default: diff --git a/pkg/manifest/anaconda_installer_iso_tree_test.go b/pkg/manifest/anaconda_installer_iso_tree_test.go index 47f201e9d4..2e2645cacd 100644 --- a/pkg/manifest/anaconda_installer_iso_tree_test.go +++ b/pkg/manifest/anaconda_installer_iso_tree_test.go @@ -17,6 +17,7 @@ import ( "github.com/osbuild/images/pkg/arch" "github.com/osbuild/images/pkg/container" "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/customizations/users" "github.com/osbuild/images/pkg/datasizes" "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/manifest" @@ -447,7 +448,7 @@ func TestAnacondaISOTreeSerializeWithOS(t *testing.T) { } pipeline.ISOBoot = manifest.SyslinuxISOBoot _, err := manifest.SerializeWith(pipeline, manifest.Inputs{}) - assert.EqualError(t, err, "cannot create ostree container stages: cannot create kickstart stages: kickstart unattended options are not compatible with user-supplied kickstart content") + assert.EqualError(t, err, "cannot create tar payload stages: cannot create kickstart stages: kickstart unattended options are not compatible with user-supplied kickstart content") }) t.Run("unhappy/user-kickstart-with-sudo-bits", func(t *testing.T) { @@ -463,7 +464,7 @@ func TestAnacondaISOTreeSerializeWithOS(t *testing.T) { } pipeline.ISOBoot = manifest.SyslinuxISOBoot _, err := manifest.SerializeWith(pipeline, manifest.Inputs{}) - assert.EqualError(t, err, "cannot create ostree container stages: cannot create kickstart stages: kickstart sudo nopasswd drop-in file creation is not compatible with user-supplied kickstart content") + assert.EqualError(t, err, "cannot create tar payload stages: cannot create kickstart stages: kickstart sudo nopasswd drop-in file creation is not compatible with user-supplied kickstart content") }) t.Run("plain+squashfs-rootfs", func(t *testing.T) { @@ -488,6 +489,38 @@ func TestAnacondaISOTreeSerializeWithOS(t *testing.T) { append(variantStages, []string{"org.osbuild.kickstart", "org.osbuild.isolinux", "org.osbuild.squashfs"}...)), dumpStages(sp.Stages)) }) + + t.Run("happy/kickstart-with-users", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.OSPipeline = osPayload + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "me", + Shell: common.ToPtr("/bin/true"), + }, + }, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{}) + assert.NoError(t, err) + }) + + t.Run("unhappy/kickstart-with-root-options", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.OSPipeline = osPayload + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/true"), + }, + }, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{}) + assert.EqualError(t, err, "cannot create tar payload stages: failed to create kickstart stage options: org.osbuild.kickstart: unsupported options for user \"root\": shell") + }) } func TestAnacondaISOTreeSerializeWithOSTree(t *testing.T) { @@ -624,6 +657,38 @@ func TestAnacondaISOTreeSerializeWithOSTree(t *testing.T) { append(variantStages, []string{"org.osbuild.isolinux", "org.osbuild.squashfs"}...)), dumpStages(sp.Stages)) }) + + t.Run("happy/kickstart-with-users", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "me", + Shell: common.ToPtr("/bin/true"), + }, + }, + OSTree: &kickstart.OSTree{}, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{Commits: []ostree.CommitSpec{ostreeCommit}}) + assert.NoError(t, err) + }) + + t.Run("unhappy/kickstart-with-root-options", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/true"), + }, + }, + OSTree: &kickstart.OSTree{}, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{Commits: []ostree.CommitSpec{ostreeCommit}}) + assert.EqualError(t, err, "cannot create ostree commit stages: failed to create kickstart stage options: org.osbuild.kickstart: unsupported options for user \"root\": shell") + }) } func makeFakeContainerPayload() container.Spec { @@ -743,6 +808,36 @@ func TestAnacondaISOTreeSerializeWithContainer(t *testing.T) { append(variantStages, []string{"org.osbuild.isolinux", "org.osbuild.squashfs"}...)), dumpStages(sp.Stages)) }) + + t.Run("happy/kickstart-with-users", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "me", + Shell: common.ToPtr("/bin/true"), + }, + }, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}}) + assert.NoError(t, err) + }) + + t.Run("unhappy/kickstart-with-root-options", func(t *testing.T) { + pipeline := newTestAnacondaISOTree() + pipeline.Kickstart = &kickstart.Options{ + Path: testKsPath, + Users: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/true"), + }, + }, + } + _, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}}) + assert.EqualError(t, err, "cannot create ostree container stages: cannot generate bootc installer kickstart stages: failed to create kickstart stage options: org.osbuild.kickstart: unsupported options for user \"root\": shell") + }) } func TestMakeKickstartSudoersPost(t *testing.T) { diff --git a/pkg/osbuild/kickstart_stage.go b/pkg/osbuild/kickstart_stage.go index 52ce7b033c..caef867cdb 100644 --- a/pkg/osbuild/kickstart_stage.go +++ b/pkg/osbuild/kickstart_stage.go @@ -3,6 +3,7 @@ package osbuild import ( "fmt" "path/filepath" + "regexp" "strings" "github.com/osbuild/images/pkg/customizations/fsnode" @@ -10,6 +11,7 @@ import ( ) const ( + kickstartStageType = "org.osbuild.kickstart" KickstartPathInteractiveDefaults = "/usr/share/anaconda/interactive-defaults.ks" KickstartPathOSBuild = "/osbuild.ks" ) @@ -132,7 +134,7 @@ func (KickstartStageOptions) isStageOptions() {} // Creates an Anaconda kickstart file func NewKickstartStage(options *KickstartStageOptions) *Stage { return &Stage{ - Type: "org.osbuild.kickstart", + Type: kickstartStageType, Options: options, } } @@ -214,6 +216,11 @@ func NewKickstartStageOptions( userCustomizations []users.User, groupCustomizations []users.Group) (*KickstartStageOptions, error) { + invalidPathRegex := regexp.MustCompile(invalidPathRegex) + if invalidPathRegex.FindAllString(path, -1) != nil { + return nil, fmt.Errorf("%s: kickstart path %q is invalid", kickstartStageType, path) + } + var users map[string]UsersStageOptionsUser if usersOptions, err := NewUsersStageOptions(userCustomizations, false); err != nil { return nil, err @@ -228,7 +235,7 @@ func NewKickstartStageOptions( rootpw, err := adjustRootUserOptions(users) if err != nil { - return nil, fmt.Errorf("org.osbuild.kickstart: %w", err) + return nil, fmt.Errorf("%s: %w", kickstartStageType, err) } return &KickstartStageOptions{ diff --git a/pkg/osbuild/kickstart_stage_test.go b/pkg/osbuild/kickstart_stage_test.go index 45371ad7fb..cee58dc9fd 100644 --- a/pkg/osbuild/kickstart_stage_test.go +++ b/pkg/osbuild/kickstart_stage_test.go @@ -194,15 +194,530 @@ func TestKickstartStageUsers(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - ksOpts, err := osbuild.NewKickstartStageOptions("", tc.users, nil) + ksOpts, err := osbuild.NewKickstartStageOptions("/test.ks", tc.users, nil) if tc.expErr != "" { assert.EqualError(err, tc.expErr) return } assert.NoError(err) + exp := tc.expected + exp.Path = "/test.ks" assert.Equal(tc.expected, ksOpts) }) } } + +func TestNewKickstartOptionsPlain(t *testing.T) { + type testCase struct { + path string + userCustomizations []users.User + groupCustomizations []users.Group + + expOptions *osbuild.KickstartStageOptions + expErr string + } + + testCases := map[string]testCase{ + "empty": { + expErr: "org.osbuild.kickstart: kickstart path \"\" is invalid", + }, + "bad-path": { + path: `C:\Wrong\Operating\System`, + expErr: `org.osbuild.kickstart: kickstart path "C:\\Wrong\\Operating\\System" is invalid`, + }, + "path-only": { + path: "/osbuild-test.ks", + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + }, + }, + "user": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + }, + }, + "user+group": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + groupCustomizations: []users.Group{ + { + Name: "staff", + GID: common.ToPtr(2000), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + Groups: map[string]osbuild.GroupsStageOptionsGroup{ + "staff": { + GID: common.ToPtr(2000), + }, + }, + }, + }, + "good-root-options": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "root", + Password: common.ToPtr("$6$BhyxFBgrEFh0VrPJ$MllG8auiU26x2pmzL4.1maHzPHrA.4gTdCvlATFp8HJU9UPee4zCS9BVl2HOzKaUYD/zEm8r/OF05F2icWB0K/"), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{}, // non-nil empty map is returned + RootPassword: &osbuild.RootPasswordOptions{ + IsCrypted: true, + Password: "$6$BhyxFBgrEFh0VrPJ$MllG8auiU26x2pmzL4.1maHzPHrA.4gTdCvlATFp8HJU9UPee4zCS9BVl2HOzKaUYD/zEm8r/OF05F2icWB0K/", + }, + }, + }, + "bad-root-options": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "root", + Groups: []string{"wheel"}, + }, + }, + expErr: "org.osbuild.kickstart: unsupported options for user \"root\": groups", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ksOptions, err := osbuild.NewKickstartStageOptions( + tc.path, + tc.userCustomizations, + tc.groupCustomizations, + ) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expOptions, ksOptions) // new file path must be the original kickstart path + }) + } +} + +func TestNewKicstartStageOptionsWithOSTreeCommit(t *testing.T) { + type testCase struct { + path string + userCustomizations []users.User + ostreeURL string + ostreeRef string + ostreeRemote string + osName string + + expOptions *osbuild.KickstartStageOptions + expErr string + } + + testCases := map[string]testCase{ + "empty": { + expErr: "org.osbuild.kickstart: kickstart path \"\" is invalid", + }, + "user": { + + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + }, + }, + "ostree": { + path: "/osbuild-test.ks", + ostreeURL: "https://example.org/internal/ostree/repo", + ostreeRef: "example/aarch64/1", + ostreeRemote: "https://example.org/prod/ostree/repo", + osName: "guess-what-its-example", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + OSTreeCommit: &osbuild.OSTreeCommitOptions{ + OSName: "guess-what-its-example", + Remote: "https://example.org/prod/ostree/repo", + URL: "https://example.org/internal/ostree/repo", + Ref: "example/aarch64/1", + GPG: false, + }, + }, + }, + "user+ostree": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + ostreeURL: "https://example.org/ostree/repo", + ostreeRef: "example/aarch64/1", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + OSTreeCommit: &osbuild.OSTreeCommitOptions{ + Ref: "example/aarch64/1", + URL: "https://example.org/ostree/repo", + }, + }, + }, + "internal-error": { + path: "/osbuild-test.ks", + // only need to check that an error from NewKickstartStageOptions + // is propagated + userCustomizations: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/tsh"), + }, + }, + expErr: "org.osbuild.kickstart: unsupported options for user \"root\": shell", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ksOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeCommit( + tc.path, + tc.userCustomizations, + nil, + tc.ostreeURL, + tc.ostreeRef, + tc.ostreeRemote, + tc.osName, + ) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expOptions, ksOptions) // new file path must be the original kickstart path + }) + } +} + +func TestNewKicstartStageOptionsWithOSTreeContainer(t *testing.T) { + type testCase struct { + path string + userCustomizations []users.User + containerURL string + containerTransport string + containerRemote string + containerStateRoot string + + expOptions *osbuild.KickstartStageOptions + expErr string + } + + testCases := map[string]testCase{ + "empty": { + expErr: "org.osbuild.kickstart: kickstart path \"\" is invalid", + }, + "user": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + }, + }, + "container": { + path: "/osbuild-test.ks", + containerURL: "https://example.org/internal/some-kind-of-container", + containerTransport: "docker", + containerRemote: "origin", + containerStateRoot: "default", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + OSTreeContainer: &osbuild.OSTreeContainerOptions{ + StateRoot: "default", + URL: "https://example.org/internal/some-kind-of-container", + Transport: "docker", + Remote: "origin", + }, + }, + }, + "user+container": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + containerURL: "https://example.org/internal/some-kind-of-container", + containerTransport: "docker", + containerRemote: "origin", + containerStateRoot: "default", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + OSTreeContainer: &osbuild.OSTreeContainerOptions{ + StateRoot: "default", + URL: "https://example.org/internal/some-kind-of-container", + Transport: "docker", + Remote: "origin", + }, + }, + }, + "internal-error": { + path: "/osbuild-test.ks", + // only need to check that an error from NewKickstartStageOptions + // is propagated + userCustomizations: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/tsh"), + }, + }, + expErr: "org.osbuild.kickstart: unsupported options for user \"root\": shell", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ksOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeContainer( + tc.path, + tc.userCustomizations, + nil, + tc.containerURL, + tc.containerTransport, + tc.containerRemote, + tc.containerStateRoot, + ) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expOptions, ksOptions) // new file path must be the original kickstart path + }) + } +} + +func TestNewKicstartStageOptionsWithLiveIMG(t *testing.T) { + type testCase struct { + path string + userCustomizations []users.User + imgURL string + + expOptions *osbuild.KickstartStageOptions + expErr string + } + + testCases := map[string]testCase{ + "empty": { + expErr: "org.osbuild.kickstart: kickstart path \"\" is invalid", + }, + "user": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + }, + }, + "img": { + path: "/osbuild-test.ks", + imgURL: "/path/to/tarball/image.tar", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + LiveIMG: &osbuild.LiveIMGOptions{ + URL: "/path/to/tarball/image.tar", + }, + }, + }, + "user+img": { + path: "/osbuild-test.ks", + userCustomizations: []users.User{ + { + Name: "fisher", + Shell: common.ToPtr("/bin/fish"), + }, + }, + imgURL: "/path/to/tarball/image.tar", + + expOptions: &osbuild.KickstartStageOptions{ + Path: "/osbuild-test.ks", + Users: map[string]osbuild.UsersStageOptionsUser{ + "fisher": { + Shell: common.ToPtr("/bin/fish"), + }, + }, + LiveIMG: &osbuild.LiveIMGOptions{ + URL: "/path/to/tarball/image.tar", + }, + }, + }, + "internal-error": { + path: "/osbuild-test.ks", + // only need to check that an error from NewKickstartStageOptions + // is propagated + userCustomizations: []users.User{ + { + Name: "root", + Shell: common.ToPtr("/bin/tsh"), + }, + }, + expErr: "org.osbuild.kickstart: unsupported options for user \"root\": shell", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + ksOptions, err := osbuild.NewKickstartStageOptionsWithLiveIMG( + tc.path, + tc.userCustomizations, + nil, + tc.imgURL, + ) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expOptions, ksOptions) // new file path must be the original kickstart path + }) + } +} + +func TestIncludeRaw(t *testing.T) { + type testCase struct { + path string + raw string + + expPath string + expData string + expErr string + } + + testCases := map[string]testCase{ + "ok-empty": { + path: "/t.ks", + raw: "", + + expPath: "/t-base.ks", + expData: "%include /run/install/repo/t-base.ks\n", + }, + "empty-path": { + expErr: "path must not be empty", + }, + "rel-path": { + path: "42.ps", + expErr: "path must be absolute", + }, + + "kickstart-command": { + path: "/my.ks", + raw: "bootc switch --mutate-in-plase --transport registry registry.example.org/my-weird-stuff:v42", + + expPath: "/my-base.ks", + expData: "%include /run/install/repo/my-base.ks\nbootc switch --mutate-in-plase --transport registry registry.example.org/my-weird-stuff:v42", + }, + "kickstart-commands": { + path: "/your.ks", + raw: "autopart --nohome --type=btrfs\neula --agreed", + + expPath: "/your-base.ks", + expData: "%include /run/install/repo/your-base.ks\nautopart --nohome --type=btrfs\neula --agreed", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + // only the path is relevant + ksOptions := &osbuild.KickstartStageOptions{ + Path: tc.path, + } + file, err := ksOptions.IncludeRaw(tc.raw) + if tc.expErr != "" { + assert.EqualError(err, tc.expErr) + return + } + + assert.NoError(err) + assert.Equal(tc.expPath, ksOptions.Path) + assert.Equal(tc.expData, string(file.Data())) + assert.Equal(tc.path, string(file.Path())) // new file path must be the original kickstart path + }) + } +}