Merge pull request #1603 from anderbubble/container-build-false

Add wwctl container <exec|shell> --build=false
This commit is contained in:
Christian Goll
2025-01-08 20:37:47 +01:00
committed by GitHub
8 changed files with 46 additions and 24 deletions

View File

@@ -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 - Support defining a symlink with an overlay template. #1303
- New "localtime" overlay to define the system time zone. #1303 - New "localtime" overlay to define the system time zone. #1303
- Add support for nested profiles. #1572, #1598 - Add support for nested profiles. #1572, #1598
- Adds `wwctl container <exec|shell> --build=false` to prevent automatically (re)building the container. #1490, #1489
### Changed ### Changed

View File

@@ -21,9 +21,6 @@ import (
) )
const exitEval = `$(VALU="$?" ; if [ $VALU == 0 ]; then echo write; else echo discard; fi)` 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) { func CobraRunE(cmd *cobra.Command, args []string) (err error) {
if os.Getpid() != 1 { 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) return fmt.Errorf("failed to mount: %w", err)
} }
ps1Str := fmt.Sprintf("[%s|%s] Warewulf> ", containerName, exitEval) ps1Str := fmt.Sprintf("[%s|%s] Warewulf> ", containerName, exitEval)
wwlog.Info(msgStr)
if len(lowerObjects) != 0 && nodename == "" { if len(lowerObjects) != 0 && nodename == "" {
options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s",
path.Join(runDir, "lower"), containerPath, path.Join(runDir, "work")) path.Join(runDir, "lower"), containerPath, path.Join(runDir, "work"))

View File

@@ -145,14 +145,16 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
if userdbChanged && SyncUser { if userdbChanged && SyncUser {
err = container.SyncUids(containerName, false) err = container.SyncUids(containerName, false)
if err != nil { 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") if Build {
err = container.Build(containerName, false) wwlog.Info("Building container image: %s", containerName)
if err != nil { err = container.Build(containerName, false)
return fmt.Errorf("could not build container %s: %s", containerName, err) if err != nil {
return fmt.Errorf("could not build container image: %s: %s", containerName, err)
}
} }
return nil return nil
} }

View File

@@ -2,15 +2,14 @@ package exec
import ( import (
"bytes" "bytes"
"os"
"os/exec" "os/exec"
"testing" "testing"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "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" "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) { func Test_Exec(t *testing.T) {
env := testenv.New(t) env := testenv.New(t)
defer env.RemoveAll(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 childCommandFunc = mockChildCmd
defer func() { defer func() {
childCommandFunc = runChildCmd childCommandFunc = runChildCmd
@@ -36,40 +35,55 @@ func Test_Exec(t *testing.T) {
name string name string
args []string args []string
stdout string stdout string
build bool
}{ }{
{ {
name: "plain test", name: "plain test",
args: []string{"test", "/bin/true"}, args: []string{"test", "/bin/true"},
stdout: `--loglevel 20 container exec __child test -- /bin/true`, stdout: `--loglevel 20 container exec __child test -- /bin/true`,
build: true,
}, },
{ {
name: "test with --bind", name: "test with --bind",
args: []string{"test", "--bind", "/tmp", "/bin/true"}, args: []string{"test", "--bind", "/tmp", "/bin/true"},
stdout: `--loglevel 20 container exec __child test --bind /tmp -- /bin/true`, stdout: `--loglevel 20 container exec __child test --bind /tmp -- /bin/true`,
build: true,
}, },
{ {
name: "test with --node", name: "test with --node",
args: []string{"test", "--node", "node1", "/bin/true"}, args: []string{"test", "--node", "node1", "/bin/true"},
stdout: `--loglevel 20 container exec __child 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", name: "test with --node and --bind",
args: []string{"test", "--bind", "/tmp", "--node", "node1", "/bin/true"}, args: []string{"test", "--bind", "/tmp", "--node", "node1", "/bin/true"},
stdout: `--loglevel 20 container exec __child 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", name: "test with complex command",
args: []string{"test", "/bin/bash", "echo 'hello'"}, args: []string{"test", "/bin/bash", "echo 'hello'"},
stdout: `--loglevel 20 container exec __child test -- /bin/bash echo 'hello'`, stdout: `--loglevel 20 container exec __child test -- /bin/bash echo 'hello'`,
build: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Logf("Running test: %s\n", tt.name)
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
defer func() { defer func() {
binds = []string{} binds = []string{}
nodeName = "" 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 := GetCommand()
cmd.SetArgs(tt.args) cmd.SetArgs(tt.args)
@@ -77,14 +91,15 @@ func Test_Exec(t *testing.T) {
err := bytes.NewBufferString("") err := bytes.NewBufferString("")
cmd.SetOut(out) cmd.SetOut(out)
cmd.SetErr(err) cmd.SetErr(err)
if err := cmd.Execute(); err != nil { assert.NoError(t, cmd.Execute())
t.Errorf("Received error when running command, err: %v", err) assert.NotEmpty(t, out.String())
t.FailNow() assert.Contains(t, out.String(), tt.stdout)
} if tt.build {
assert.NotEmpty(t, out.String(), "os.stdout should not be empty") assert.FileExists(t, env.GetPath("/srv/warewulf/container/test.img"))
if !strings.Contains(out.String(), tt.stdout) { assert.FileExists(t, env.GetPath("/srv/warewulf/container/test.img.gz"))
t.Errorf("Got wrong output, got:\n '%s'\n, but want:\n '%s'\n", out.String(), tt.stdout) } else {
t.FailNow() assert.NoFileExists(t, env.GetPath("/srv/warewulf/container/test.img"))
assert.NoFileExists(t, env.GetPath("/srv/warewulf/container/test.img.gz"))
} }
}) })
} }

View File

@@ -26,6 +26,7 @@ var (
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
} }
SyncUser bool SyncUser bool
Build bool
binds []string binds []string
nodeName 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 set, uses the same path as source. "ro" binds read-only. "copy" temporarily
copies the file into the container.`) copies the file into the container.`)
baseCmd.PersistentFlags().BoolVar(&SyncUser, "syncuser", false, "Synchronize UIDs/GIDs from host to 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") baseCmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "Create a read only view of the container for the given node")
} }

View File

@@ -44,5 +44,9 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
cntexec.SetBinds(binds) cntexec.SetBinds(binds)
cntexec.SetNode(nodeName) cntexec.SetNode(nodeName)
cntexec.SyncUser = syncUser 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) return cntexec.CobraRunE(cmd, allargs)
} }

View File

@@ -26,6 +26,7 @@ var (
binds []string binds []string
nodeName string nodeName string
syncUser bool syncUser bool
build bool
) )
func init() { 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.`) 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().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(&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. // GetRootCommand returns the root cobra.Command for the application.

View File

@@ -229,7 +229,7 @@ when using the exec command. This works as follows:
.. code-block:: console .. code-block:: console
# wwctl container exec --bind /tmp:/mnt rocky-8 /bin/sh # wwctl container shell --bind /tmp:/mnt rocky-8
[rocky-8] Warewulf> [rocky-8] Warewulf>
.. note:: .. note::
@@ -257,7 +257,7 @@ can be specified in ``warewulf.conf``:
When the command completes, if anything within the container changed, When the command completes, if anything within the container changed,
the container will be rebuilt into a bootable static object 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 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 will be an additional check to confirm if the users are in sync as