Merge pull request #1603 from anderbubble/container-build-false
Add wwctl container <exec|shell> --build=false
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user