diff --git a/CHANGELOG.md b/CHANGELOG.md index ad3e53e5..dbb1c831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support defining a symlink with an overlay template. #1303 - New "localtime" overlay to define the system time zone. #1303 - Add support for nested profiles. #1572, #1598 +- Adds `wwctl container --build=false` to prevent automatically (re)building the container. #1490, #1489 ### Changed diff --git a/internal/app/wwctl/container/exec/child/main.go b/internal/app/wwctl/container/exec/child/main.go index 52c461b5..7847616d 100644 --- a/internal/app/wwctl/container/exec/child/main.go +++ b/internal/app/wwctl/container/exec/child/main.go @@ -21,9 +21,6 @@ import ( ) const exitEval = `$(VALU="$?" ; if [ $VALU == 0 ]; then echo write; else echo discard; fi)` -const msgStr = `Container image is rebuilt depending on the exit status of the last command. - -Run "true" or "false" to enforce or abort image rebuild.` func CobraRunE(cmd *cobra.Command, args []string) (err error) { if os.Getpid() != 1 { @@ -82,7 +79,6 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { return fmt.Errorf("failed to mount: %w", err) } ps1Str := fmt.Sprintf("[%s|%s] Warewulf> ", containerName, exitEval) - wwlog.Info(msgStr) if len(lowerObjects) != 0 && nodename == "" { options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", path.Join(runDir, "lower"), containerPath, path.Join(runDir, "work")) diff --git a/internal/app/wwctl/container/exec/main.go b/internal/app/wwctl/container/exec/main.go index 148fc66e..51f2993f 100644 --- a/internal/app/wwctl/container/exec/main.go +++ b/internal/app/wwctl/container/exec/main.go @@ -145,14 +145,16 @@ func CobraRunE(cmd *cobra.Command, args []string) error { if userdbChanged && SyncUser { err = container.SyncUids(containerName, false) if err != nil { - wwlog.Error("Error in user sync, fix error and run 'syncuser' manually, but trying to build container: %s", err) + wwlog.Error("Error in user sync, fix error and run 'syncuser' manually: %s", err) } } - fmt.Printf("Rebuilding container...\n") - err = container.Build(containerName, false) - if err != nil { - return fmt.Errorf("could not build container %s: %s", containerName, err) + if Build { + wwlog.Info("Building container image: %s", containerName) + err = container.Build(containerName, false) + if err != nil { + return fmt.Errorf("could not build container image: %s: %s", containerName, err) + } } return nil } diff --git a/internal/app/wwctl/container/exec/main_test.go b/internal/app/wwctl/container/exec/main_test.go index bf3af8c2..134bbc0b 100644 --- a/internal/app/wwctl/container/exec/main_test.go +++ b/internal/app/wwctl/container/exec/main_test.go @@ -2,15 +2,14 @@ package exec import ( "bytes" + "os" "os/exec" "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" - "github.com/warewulf/warewulf/internal/pkg/testenv" - "path" - "strings" + "github.com/warewulf/warewulf/internal/pkg/testenv" "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) @@ -25,7 +24,7 @@ func mockChildCmd(cmd *cobra.Command, args []string) error { func Test_Exec(t *testing.T) { env := testenv.New(t) defer env.RemoveAll(t) - env.MkdirAll(t, path.Join(testenv.WWChrootdir, "test/rootfs")) + env.MkdirAll(t, "/var/lib/warewulf/chroots/test/rootfs") childCommandFunc = mockChildCmd defer func() { childCommandFunc = runChildCmd @@ -36,40 +35,55 @@ func Test_Exec(t *testing.T) { name string args []string stdout string + build bool }{ { name: "plain test", args: []string{"test", "/bin/true"}, stdout: `--loglevel 20 container exec __child test -- /bin/true`, + build: true, }, { name: "test with --bind", args: []string{"test", "--bind", "/tmp", "/bin/true"}, stdout: `--loglevel 20 container exec __child test --bind /tmp -- /bin/true`, + build: true, }, { name: "test with --node", args: []string{"test", "--node", "node1", "/bin/true"}, stdout: `--loglevel 20 container exec __child test --node node1 -- /bin/true`, + build: true, + }, + { + name: "test with --build=false", + args: []string{"test", "--build=false", "/bin/true"}, + stdout: `--loglevel 20 container exec __child test -- /bin/true`, + build: false, }, { name: "test with --node and --bind", args: []string{"test", "--bind", "/tmp", "--node", "node1", "/bin/true"}, stdout: `--loglevel 20 container exec __child test --bind /tmp --node node1 -- /bin/true`, + build: true, }, { name: "test with complex command", args: []string{"test", "/bin/bash", "echo 'hello'"}, stdout: `--loglevel 20 container exec __child test -- /bin/bash echo 'hello'`, + build: true, }, } for _, tt := range tests { - t.Logf("Running test: %s\n", tt.name) t.Run(tt.name, func(t *testing.T) { defer func() { binds = []string{} nodeName = "" + Build = true + SyncUser = false + os.Remove(env.GetPath("/srv/warewulf/container/test.img")) + os.Remove(env.GetPath("/srv/warewulf/container/test.img.gz")) }() cmd := GetCommand() cmd.SetArgs(tt.args) @@ -77,14 +91,15 @@ func Test_Exec(t *testing.T) { err := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(err) - if err := cmd.Execute(); err != nil { - t.Errorf("Received error when running command, err: %v", err) - t.FailNow() - } - assert.NotEmpty(t, out.String(), "os.stdout should not be empty") - if !strings.Contains(out.String(), tt.stdout) { - t.Errorf("Got wrong output, got:\n '%s'\n, but want:\n '%s'\n", out.String(), tt.stdout) - t.FailNow() + assert.NoError(t, cmd.Execute()) + assert.NotEmpty(t, out.String()) + assert.Contains(t, out.String(), tt.stdout) + if tt.build { + assert.FileExists(t, env.GetPath("/srv/warewulf/container/test.img")) + assert.FileExists(t, env.GetPath("/srv/warewulf/container/test.img.gz")) + } else { + assert.NoFileExists(t, env.GetPath("/srv/warewulf/container/test.img")) + assert.NoFileExists(t, env.GetPath("/srv/warewulf/container/test.img.gz")) } }) } diff --git a/internal/app/wwctl/container/exec/root.go b/internal/app/wwctl/container/exec/root.go index 1152fd8e..f5909878 100644 --- a/internal/app/wwctl/container/exec/root.go +++ b/internal/app/wwctl/container/exec/root.go @@ -26,6 +26,7 @@ var ( FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, } SyncUser bool + Build bool binds []string nodeName string ) @@ -37,6 +38,7 @@ Bind a local path which must exist into the container. If destination is not set, uses the same path as source. "ro" binds read-only. "copy" temporarily copies the file into the container.`) baseCmd.PersistentFlags().BoolVar(&SyncUser, "syncuser", false, "Synchronize UIDs/GIDs from host to container") + baseCmd.PersistentFlags().BoolVar(&Build, "build", true, "(Re)build the container image automatically") baseCmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "Create a read only view of the container for the given node") } diff --git a/internal/app/wwctl/container/shell/main.go b/internal/app/wwctl/container/shell/main.go index b92779c4..54911323 100644 --- a/internal/app/wwctl/container/shell/main.go +++ b/internal/app/wwctl/container/shell/main.go @@ -44,5 +44,9 @@ func CobraRunE(cmd *cobra.Command, args []string) error { cntexec.SetBinds(binds) cntexec.SetNode(nodeName) cntexec.SyncUser = syncUser + cntexec.Build = build + if cntexec.Build { + wwlog.Info("Container image build will be skipped if the shell ends with a non-zero exit code.") + } return cntexec.CobraRunE(cmd, allargs) } diff --git a/internal/app/wwctl/container/shell/root.go b/internal/app/wwctl/container/shell/root.go index b6d2e60b..b01d48c8 100644 --- a/internal/app/wwctl/container/shell/root.go +++ b/internal/app/wwctl/container/shell/root.go @@ -26,6 +26,7 @@ var ( binds []string nodeName string syncUser bool + build bool ) func init() { @@ -35,6 +36,7 @@ set, uses the same path as source. "ro" binds read-only. "copy" temporarily copies the file into the container.`) baseCmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "Create a read only view of the container for the given node") baseCmd.PersistentFlags().BoolVar(&syncUser, "syncuser", false, "Synchronize UIDs/GIDs from host to container") + baseCmd.PersistentFlags().BoolVar(&build, "build", true, "(Re)build the container image automatically") } // GetRootCommand returns the root cobra.Command for the application. diff --git a/userdocs/contents/containers.rst b/userdocs/contents/containers.rst index bbfbc147..c68de260 100644 --- a/userdocs/contents/containers.rst +++ b/userdocs/contents/containers.rst @@ -229,7 +229,7 @@ when using the exec command. This works as follows: .. code-block:: console - # wwctl container exec --bind /tmp:/mnt rocky-8 /bin/sh + # wwctl container shell --bind /tmp:/mnt rocky-8 [rocky-8] Warewulf> .. note:: @@ -257,7 +257,7 @@ can be specified in ``warewulf.conf``: When the command completes, if anything within the container changed, the container will be rebuilt into a bootable static object -automatically. +automatically. (To skip the automatic container rebuild, specify ``--build=false``.) If the files ``/etc/passwd`` or ``/etc/group`` were updated, there will be an additional check to confirm if the users are in sync as