diff --git a/doc/toolbox-create.1.md b/doc/toolbox-create.1.md index b2dacb303..00be22185 100644 --- a/doc/toolbox-create.1.md +++ b/doc/toolbox-create.1.md @@ -8,6 +8,8 @@ toolbox\-create - Create a new Toolbx container [*--distro DISTRO* | *-d DISTRO*] [*--image NAME* | *-i NAME*] [*--release RELEASE* | *-r RELEASE*] + [*--build BUILDCONTEXT* | *-b BUILDCONTEXT*] + [*--build-tag TAG* | *-t TAG*] [*CONTAINER*] ## DESCRIPTION @@ -110,6 +112,22 @@ remote registry. Create a Toolbx container for a different operating system RELEASE than the host. Cannot be used with `--image`. +**--build** BUILDCONTEXT, **-b** BUILDCONTEXT + +Build a toolbx image from the build context found at BUILDCONTEXT by passing it +to `podman build`. Afterwards it sets the tag to `localhost/` +by extracting the name from the image and then creates the container like normal. + +You cannot use `--distro`, `--release` or `--image` together with this option. + +**--build-tag** TAG, **-t** TAG + +Overwrites the tagging behaviour of `--build` by tagging the image with TAG via +`podman build --tag`. If no repository if given or podman doesn't know it, +localhost is used. + +Can only be used when `--build` is also used. + ## EXAMPLES ### Create the default Toolbx container matching the host OS diff --git a/src/cmd/create.go b/src/cmd/create.go index 247b8097f..4313c38ed 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -54,6 +54,8 @@ var ( distro string image string release string + build string + buildtag string } createToolboxShMounts = []struct { @@ -104,6 +106,18 @@ func init() { "", "Create a Toolbx container for a different operating system release than the host") + flags.StringVarP(&createFlags.build, + "build", + "b", + "", + "Build a Toolbx container for use of this container") + + flags.StringVarP(&createFlags.buildtag, + "build-tag", + "t", + "", + "Tag the image built") + createCmd.SetHelpFunc(createHelp) if err := createCmd.RegisterFlagCompletionFunc("distro", completionDistroNames); err != nil { @@ -150,6 +164,24 @@ func create(cmd *cobra.Command, args []string) error { return errors.New(errMsg) } + if cmd.Flag("build").Changed && (cmd.Flag("image").Changed || cmd.Flag("release").Changed || cmd.Flag("distro").Changed) { + var builder strings.Builder + fmt.Fprintf(&builder, "options --build and --release, --image or -- distro cannot be used together\n") + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) + } + + if cmd.Flag("build-tag").Changed && !cmd.Flag("build").Changed { + var builder strings.Builder + fmt.Fprintf(&builder, "--build-tag must be used together with --build\n") + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) + } + if cmd.Flag("authfile").Changed { if !utils.PathExists(createFlags.authFile) { var builder strings.Builder @@ -177,7 +209,8 @@ func create(cmd *cobra.Command, args []string) error { containerArg, createFlags.distro, createFlags.image, - createFlags.release) + createFlags.release, + podman.BuildOptions{Context: createFlags.build, Tag: createFlags.buildtag}) if err != nil { return err diff --git a/src/cmd/enter.go b/src/cmd/enter.go index c82ca816e..5ed8bd826 100644 --- a/src/cmd/enter.go +++ b/src/cmd/enter.go @@ -21,6 +21,7 @@ import ( "fmt" "os" + "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) @@ -111,7 +112,8 @@ func enter(cmd *cobra.Command, args []string) error { containerArg, enterFlags.distro, "", - enterFlags.release) + enterFlags.release, + podman.BuildOptions{Context: "", Tag: ""}) if err != nil { return err diff --git a/src/cmd/rootMigrationPath.go b/src/cmd/rootMigrationPath.go index da11a580e..2c1b42e82 100644 --- a/src/cmd/rootMigrationPath.go +++ b/src/cmd/rootMigrationPath.go @@ -25,6 +25,7 @@ import ( "os" "strings" + "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) @@ -60,7 +61,7 @@ func rootRunImpl(cmd *cobra.Command, args []string) error { return nil } - container, image, release, err := resolveContainerAndImageNames("", "", "", "", "") + container, image, release, err := resolveContainerAndImageNames("", "", "", "", "", podman.BuildOptions{Context: "", Tag: ""}) if err != nil { return err } diff --git a/src/cmd/run.go b/src/cmd/run.go index 85d90bcbe..9cddb07ec 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -147,7 +147,8 @@ func run(cmd *cobra.Command, args []string) error { "--container", runFlags.distro, "", - runFlags.release) + runFlags.release, + podman.BuildOptions{Context: "", Tag: ""}) if err != nil { return err diff --git a/src/cmd/utils.go b/src/cmd/utils.go index c5c35235a..2cb4bc191 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -31,6 +31,7 @@ import ( "strings" "syscall" + "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/utils" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" @@ -402,13 +403,26 @@ func poll(pollFn pollFunc, eventFD int32, fds ...int32) error { } } -func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string) ( +func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string, buildCLI podman.BuildOptions) ( string, string, string, error, ) { - container, image, release, err := utils.ResolveContainerAndImageNames(container, - distroCLI, - imageCLI, - releaseCLI) + var image, release string + var err error + if buildCLI.Context == "" { + container, image, release, err = utils.ResolveContainerAndImageNames(container, + distroCLI, + imageCLI, + releaseCLI) + } else { + image, err = podman.BuildImage(buildCLI) + if err != nil { + return "", "", "", err + } + container, image, release, err = utils.ResolveContainerAndImageNames(container, + distroCLI, + image, + releaseCLI) + } if err != nil { var errContainer *utils.ContainerError diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go index 4711b8b5c..45c077457 100644 --- a/src/pkg/podman/podman.go +++ b/src/pkg/podman/podman.go @@ -23,7 +23,9 @@ import ( "errors" "fmt" "io" + "os" "strconv" + "strings" "time" "github.com/HarryMichal/go-version" @@ -39,6 +41,11 @@ type Image struct { Names []string } +type BuildOptions struct { + Context string + Tag string +} + type ImageSlice []Image var ( @@ -53,6 +60,12 @@ var ( LogLevel = logrus.ErrorLevel ) +var ( + ErrBuildContextDoesNotExist = errors.New("build context does not exist") + + ErrBuildContextInvalid = errors.New("build context is not a directory with a Containerfile") +) + func (image *Image) FlattenNames(fillNameWithID bool) []Image { var ret []Image @@ -129,6 +142,52 @@ func (images ImageSlice) Swap(i, j int) { images[i], images[j] = images[j], images[i] } +func BuildImage(build BuildOptions) (string, error) { + if !utils.PathExists(build.Context) { + return "", &utils.BuildError{BuildContext: build.Context, Err: ErrBuildContextDoesNotExist} + } + if stat, err := os.Stat(build.Context); err != nil { + return "", err + } else { + if !stat.Mode().IsDir() { + return "", &utils.BuildError{BuildContext: build.Context, Err: ErrBuildContextInvalid} + } + } + if !utils.PathExists(build.Context+"/Containerfile") && !utils.PathExists(build.Context+"/Dockerfile") { + return "", &utils.BuildError{BuildContext: build.Context, Err: ErrBuildContextInvalid} + } + logLevelString := LogLevel.String() + args := []string{"--log-level", logLevelString, "build", build.Context} + if build.Tag != "" { + args = append(args, "--tag", build.Tag) + } + + stdout := new(bytes.Buffer) + if err := shell.Run("podman", nil, stdout, nil, args...); err != nil { + return "", err + } + output := strings.TrimRight(stdout.String(), "\n") + imageIdBegin := strings.LastIndex(output, "\n") + 1 + imageId := output[imageIdBegin:] + + var name string + if build.Tag == "" { + info, err := InspectImage(imageId) + if err != nil { + return "", err + } + name = info["Labels"].(map[string]interface{})["name"].(string) + args = []string{"--log-level", logLevelString, "tag", imageId, name} + if err := shell.Run("podman", nil, nil, nil, args...); err != nil { + return "", err + } + } else { + name = build.Tag + } + + return name, nil +} + // CheckVersion compares provided version with the version of Podman. // // Takes in one string parameter that should be in the format that is used for versioning (eg. 1.0.0, 2.5.1-dev). diff --git a/src/pkg/shell/shell.go b/src/pkg/shell/shell.go index 0eb4c941f..6b964e874 100644 --- a/src/pkg/shell/shell.go +++ b/src/pkg/shell/shell.go @@ -39,7 +39,7 @@ func RunContext(ctx context.Context, name string, stdin io.Reader, stdout, stder return err } if exitCode != 0 { - return fmt.Errorf("failed to invoke %s(1)", name) + return fmt.Errorf("failed to invoke %s(%d)", name, exitCode) } return nil } diff --git a/src/pkg/utils/errors.go b/src/pkg/utils/errors.go index a73fdf00b..921a544db 100644 --- a/src/pkg/utils/errors.go +++ b/src/pkg/utils/errors.go @@ -40,6 +40,11 @@ type ParseReleaseError struct { Hint string } +type BuildError struct { + BuildContext string + Err error +} + func (err *ContainerError) Error() string { errMsg := fmt.Sprintf("%s: %s", err.Container, err.Err) return errMsg @@ -70,3 +75,8 @@ func (err *ImageError) Unwrap() error { func (err *ParseReleaseError) Error() string { return err.Hint } + +func (err *BuildError) Error() string { + errMsg := fmt.Sprintf("%s: %s", err.BuildContext, err.Err) + return errMsg +} diff --git a/test/system/101-create.bats b/test/system/101-create.bats index 00572a289..00cec6b4b 100644 --- a/test/system/101-create.bats +++ b/test/system/101-create.bats @@ -841,3 +841,54 @@ teardown() { assert_line --index 1 "Enter with: toolbox enter fedora-toolbox-34" assert [ ${#lines[@]} -eq 2 ] } + +@test "create: Build an image before creating the toolbox" { + local build_context="./images/fedora/f38" + + run "$TOOLBX" create --build "$build_context" + if [ "$status" -ne 0 ] + then + echo "$output" + fi + + assert_line --index 0 "Created container: fedora-toolbox" + assert_line --index 1 "Enter with: toolbox enter fedora-toolbox" + assert [ ${#lines[@]} -eq 2 ] + + run $PODMAN images --filter reference=localhost/fedora-toolbox + assert_success + assert [ ${#lines[@]} -eq 2 ] +} + +@test "create: Build an image and tag it before creating the toolbox without repository" { + local build_context="./images/fedora/f38" + local build_tag="testbuild" + + run "$TOOLBX" create --build "$build_context" --build-tag "$build_tag" + assert_success + + assert_line --index 0 "Created container: $build_tag" + assert_line --index 1 "Enter with: toolbox enter $build_tag" + assert [ ${#lines[@]} -eq 2 ] + + run $PODMAN images --filter reference="localhost/$build_tag" + assert_success + assert [ ${#lines[@]} -eq 2 ] +} + +@test "create: Build an image and tag it before creating the toolbox with repository" { + local build_context="./images/fedora/f38" + local tag_repository="registry.fedoraproject.org" + local build_tag="testbuild" + + run "$TOOLBX" create --build "$build_context" --build-tag "$tag_repository/$build_tag" + assert_success + + assert_line --index 0 "Created container: $build_tag" + assert_line --index 1 "Enter with: toolbox enter $build_tag" + assert [ ${#lines[@]} -eq 2 ] + + run $PODMAN images --filter reference="$tag_repository/$build_tag" + assert_success + assert [ ${#lines[@]} -eq 2 ] +}