diff --git a/cmd/podman/containers/commit.go b/cmd/podman/containers/commit.go
index fa0621a4cc..c771d4c788 100644
--- a/cmd/podman/containers/commit.go
+++ b/cmd/podman/containers/commit.go
@@ -9,6 +9,7 @@ import (
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/common"
"github.com/containers/podman/v4/cmd/podman/registry"
+ "github.com/containers/podman/v4/pkg/api/handlers"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/spf13/cobra"
)
@@ -47,7 +48,7 @@ var (
commitOptions = entities.CommitOptions{
ImageName: "",
}
- iidFile string
+ configFile, iidFile string
)
func commitFlags(cmd *cobra.Command) {
@@ -57,6 +58,10 @@ func commitFlags(cmd *cobra.Command) {
flags.StringArrayVarP(&commitOptions.Changes, changeFlagName, "c", []string{}, "Apply the following possible instructions to the created image (default []): "+strings.Join(common.ChangeCmds, " | "))
_ = cmd.RegisterFlagCompletionFunc(changeFlagName, common.AutocompleteChangeInstructions)
+ configFileFlagName := "config"
+ flags.StringVar(&configFile, configFileFlagName, "", "`file` containing a container configuration to merge into the image")
+ _ = cmd.RegisterFlagCompletionFunc(configFileFlagName, completion.AutocompleteDefault)
+
formatFlagName := "format"
flags.StringVarP(&commitOptions.Format, formatFlagName, "f", "oci", "`Format` of the image manifest and metadata")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteImageFormat)
@@ -100,7 +105,16 @@ func commit(cmd *cobra.Command, args []string) error {
if !commitOptions.Quiet {
commitOptions.Writer = os.Stderr
}
-
+ if len(commitOptions.Changes) > 0 {
+ commitOptions.Changes = handlers.DecodeChanges(commitOptions.Changes)
+ }
+ if len(configFile) > 0 {
+ cfg, err := os.ReadFile(configFile)
+ if err != nil {
+ return fmt.Errorf("--config: %w", err)
+ }
+ commitOptions.Config = cfg
+ }
response, err := registry.ContainerEngine().ContainerCommit(context.Background(), container, commitOptions)
if err != nil {
return err
diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go
index 2da313c68a..6d97ea7d65 100644
--- a/cmd/podman/containers/create.go
+++ b/cmd/podman/containers/create.go
@@ -400,7 +400,7 @@ func createPodIfNecessary(cmd *cobra.Command, s *specgen.SpecGenerator, netOpts
var err error
uns := specgen.Namespace{NSMode: specgen.Default}
if cliVals.UserNS != "" {
- uns, err = specgen.ParseNamespace(cliVals.UserNS)
+ uns, err = specgen.ParseUserNamespace(cliVals.UserNS)
if err != nil {
return err
}
diff --git a/docs/source/markdown/options/volume.image.md b/docs/source/markdown/options/volume.image.md
index 7549b47ea3..b4715f8545 100644
--- a/docs/source/markdown/options/volume.image.md
+++ b/docs/source/markdown/options/volume.image.md
@@ -4,9 +4,8 @@
####> are applicable to all of those.
#### **--volume**, **-v**=*[HOST-DIR:CONTAINER-DIR[:OPTIONS]]*
-Create a bind mount. Specifying the `-v /HOST-DIR:/CONTAINER-DIR` option, Podman
-bind mounts `/HOST-DIR` from the host to `/CONTAINER-DIR` in the Podman
-container.
+Mount a host directory into containers when executing RUN instructions during
+the build.
The `OPTIONS` are a comma-separated list and can be: [[1]](#Footnote1)
@@ -17,12 +16,9 @@ The `OPTIONS` are a comma-separated list and can be: [[1]](#Footnote1) 0 {
tag = query.Tag
}
@@ -494,7 +501,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) {
options.Author = query.Author
options.Pause = query.Pause
options.Squash = query.Squash
- options.Changes = query.Changes
+ options.Changes = handlers.DecodeChanges(query.Changes)
ctr, err := runtime.LookupContainer(query.Container)
if err != nil {
utils.Error(w, http.StatusNotFound, err)
diff --git a/pkg/bindings/containers/commit.go b/pkg/bindings/containers/commit.go
index 5138b13cb6..6d094a2ff8 100644
--- a/pkg/bindings/containers/commit.go
+++ b/pkg/bindings/containers/commit.go
@@ -33,7 +33,11 @@ func Commit(ctx context.Context, nameOrID string, options *CommitOptions) (entit
return entities.IDResponse{}, err
}
params.Set("container", nameOrID)
- response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/commit", params, nil)
+ var requestBody io.Reader
+ if options.Config != nil {
+ requestBody = *options.Config
+ }
+ response, err := conn.DoRequest(ctx, requestBody, http.MethodPost, "/commit", params, nil)
if err != nil {
return id, err
}
diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go
index 6678a86ff3..ee2fe4b94b 100644
--- a/pkg/bindings/containers/types.go
+++ b/pkg/bindings/containers/types.go
@@ -29,6 +29,7 @@ type LogOptions struct {
type CommitOptions struct {
Author *string
Changes []string
+ Config *io.Reader `schema:"-"`
Comment *string
Format *string
Pause *bool
diff --git a/pkg/bindings/containers/types_commit_options.go b/pkg/bindings/containers/types_commit_options.go
index d58630b924..20e59f4d50 100644
--- a/pkg/bindings/containers/types_commit_options.go
+++ b/pkg/bindings/containers/types_commit_options.go
@@ -2,6 +2,7 @@
package containers
import (
+ "io"
"net/url"
"github.com/containers/podman/v4/pkg/bindings/internal/util"
@@ -47,6 +48,21 @@ func (o *CommitOptions) GetChanges() []string {
return o.Changes
}
+// WithConfig set field Config to given value
+func (o *CommitOptions) WithConfig(value io.Reader) *CommitOptions {
+ o.Config = &value
+ return o
+}
+
+// GetConfig returns value of field Config
+func (o *CommitOptions) GetConfig() io.Reader {
+ if o.Config == nil {
+ var z io.Reader
+ return z
+ }
+ return *o.Config
+}
+
// WithComment set field Comment to given value
func (o *CommitOptions) WithComment(value string) *CommitOptions {
o.Comment = &value
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index a47b9ed20c..44cf3fc517 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -164,6 +164,7 @@ type ContainerStatReport struct {
type CommitOptions struct {
Author string
Changes []string
+ Config []byte
Format string
ImageName string
IncludeVolumes bool
diff --git a/pkg/domain/infra/abi/config.go b/pkg/domain/infra/abi/config.go
new file mode 100644
index 0000000000..ae564cf442
--- /dev/null
+++ b/pkg/domain/infra/abi/config.go
@@ -0,0 +1,22 @@
+package abi
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+
+ "github.com/containers/image/v5/manifest"
+)
+
+// DecodeOverrideConfig reads a Schema2Config from a Reader, suppressing EOF
+// errors.
+func DecodeOverrideConfig(reader io.Reader) (*manifest.Schema2Config, error) {
+ config := manifest.Schema2Config{}
+ if reader != nil {
+ err := json.NewDecoder(reader).Decode(&config)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return nil, err
+ }
+ }
+ return &config, nil
+}
diff --git a/pkg/domain/infra/abi/config_test.go b/pkg/domain/infra/abi/config_test.go
new file mode 100644
index 0000000000..4a9af42e43
--- /dev/null
+++ b/pkg/domain/infra/abi/config_test.go
@@ -0,0 +1,56 @@
+package abi
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/containers/image/v5/manifest"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDecodeOverrideConfig(t *testing.T) {
+ testCases := []struct {
+ description string
+ body string
+ expectedValue *manifest.Schema2Config
+ expectedError bool
+ }{
+ {
+ description: "nothing",
+ body: ``,
+ expectedValue: &manifest.Schema2Config{},
+ },
+ {
+ description: "empty",
+ body: `{}`,
+ expectedValue: &manifest.Schema2Config{},
+ },
+ {
+ description: "user",
+ body: `{"User":"0:0"}`,
+ expectedValue: &manifest.Schema2Config{User: "0:0"},
+ },
+ {
+ description: "malformed",
+ body: `{"User":`,
+ expectedError: true,
+ },
+ }
+ t.Run("no reader", func(t *testing.T) {
+ value, err := DecodeOverrideConfig(nil)
+ assert.NoErrorf(t, err, "decoding nothing")
+ assert.NotNilf(t, value, "decoded value was unexpectedly nil")
+ })
+ for _, testCase := range testCases {
+ t.Run(testCase.description, func(t *testing.T) {
+ value, err := DecodeOverrideConfig(strings.NewReader(testCase.body))
+ if testCase.expectedError {
+ assert.Errorf(t, err, "decoding sample data")
+ } else {
+ assert.NoErrorf(t, err, "decoding sample data")
+ assert.NotNilf(t, value, "decoded value was unexpectedly nil")
+ assert.Equalf(t, *testCase.expectedValue, *value, "decoded value was not what we expected")
+ }
+ })
+ }
+}
diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go
index b495bc91b7..6f6d86668f 100644
--- a/pkg/domain/infra/abi/containers.go
+++ b/pkg/domain/infra/abi/containers.go
@@ -1,6 +1,7 @@
package abi
import (
+ "bytes"
"context"
"errors"
"fmt"
@@ -17,6 +18,7 @@ import (
"github.com/containers/podman/v4/libpod"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/libpod/logs"
+ "github.com/containers/podman/v4/pkg/api/handlers"
"github.com/containers/podman/v4/pkg/checkpoint"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/domain/entities/reports"
@@ -581,18 +583,29 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrID string,
}
sc := ic.Libpod.SystemContext()
+ var changes []string
+ if len(options.Changes) > 0 {
+ changes = handlers.DecodeChanges(options.Changes)
+ }
+ var overrideConfig *manifest.Schema2Config
+ if len(options.Config) > 0 {
+ if overrideConfig, err = DecodeOverrideConfig(bytes.NewReader(options.Config)); err != nil {
+ return nil, err
+ }
+ }
coptions := buildah.CommitOptions{
SignaturePolicyPath: rtc.Engine.SignaturePolicyPath,
ReportWriter: options.Writer,
SystemContext: sc,
PreferredManifestType: mimeType,
+ OverrideConfig: overrideConfig,
}
opts := libpod.ContainerCommitOptions{
CommitOptions: coptions,
Pause: options.Pause,
IncludeVolumes: options.IncludeVolumes,
Message: options.Message,
- Changes: options.Changes,
+ Changes: changes,
Author: options.Author,
Squash: options.Squash,
}
diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go
index d57d2cd669..aec85bc5a5 100644
--- a/pkg/domain/infra/tunnel/containers.go
+++ b/pkg/domain/infra/tunnel/containers.go
@@ -1,6 +1,7 @@
package tunnel
import (
+ "bytes"
"context"
"errors"
"fmt"
@@ -347,7 +348,15 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrID string,
return nil, fmt.Errorf("invalid image name %q", opts.ImageName)
}
}
- options := new(containers.CommitOptions).WithAuthor(opts.Author).WithChanges(opts.Changes).WithComment(opts.Message).WithSquash(opts.Squash).WithStream(!opts.Quiet)
+ var changes []string
+ if len(opts.Changes) > 0 {
+ changes = handlers.DecodeChanges(opts.Changes)
+ }
+ var configReader io.Reader
+ if len(opts.Config) > 0 {
+ configReader = bytes.NewReader(opts.Config)
+ }
+ options := new(containers.CommitOptions).WithAuthor(opts.Author).WithChanges(changes).WithComment(opts.Message).WithConfig(configReader).WithSquash(opts.Squash).WithStream(!opts.Quiet)
options.WithFormat(opts.Format).WithPause(opts.Pause).WithRepo(repo).WithTag(tag)
response, err := containers.Commit(ic.ClientCtx, nameOrID, options)
if err != nil {
diff --git a/test/apiv2/14-commit.at b/test/apiv2/14-commit.at
new file mode 100644
index 0000000000..1c4b11314f
--- /dev/null
+++ b/test/apiv2/14-commit.at
@@ -0,0 +1,30 @@
+# Create a container for testing the container initializing later
+podman create -t -i --name myctr $IMAGE ls
+
+config=$(mktemp -t config.XXXXXXXXXX.json)
+cat > "$config" <<- EOF
+{
+ "Entrypoint": ["/bin/crash"],
+ "Cmd": ["and", "burn"],
+ "Labels": {"for": "ever", "and": "ever"}
+}
+EOF
+
+# Create a new image based on the container
+t POST 'libpod/commit?container=myctr&repo=nativeimage&tag=1' $config 200
+
+# Check some things
+t GET libpod/images/nativeimage:1/json 200 ".Config.Cmd=$(jq .Cmd $config)" ".Config.Entrypoint=$(jq .Entrypoint $config)"
+
+# Create a new image based on the container
+t POST 'commit?container=myctr&repo=compatimage&tag=1' $config 201
+
+# Check some things
+t GET images/compatimage:1/json 200 ".Config.Cmd=$(jq .Cmd $config)" ".Config.Entrypoint=$(jq .Entrypoint $config)"
+
+# Clean up
+t DELETE containers/myctr 204
+t DELETE images/nativeimage:1 200
+t DELETE images/compatimage:1 200
+rm -f "$config"
+unset config
diff --git a/test/e2e/commit_test.go b/test/e2e/commit_test.go
index 6a9a7e614f..b1bbb29b61 100644
--- a/test/e2e/commit_test.go
+++ b/test/e2e/commit_test.go
@@ -21,7 +21,7 @@ var _ = Describe("Podman commit", func() {
session := podmanTest.Podman([]string{"commit", "test1", "--change", "BOGUS=foo", "foobar.com/test1-image:latest"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(125))
- Expect(session.ErrorToString()).To(Equal("Error: invalid change \"BOGUS=foo\" - invalid instruction BOGUS"))
+ Expect(session.ErrorToString()).To(HaveSuffix(`applying changes: processing change "BOGUS foo": did not understand change instruction "BOGUS foo"`))
session = podmanTest.Podman([]string{"commit", "test1", "foobar.com/test1-image:latest"})
session.WaitWithDefaultTimeout()
@@ -127,6 +127,45 @@ var _ = Describe("Podman commit", func() {
Expect(inspectResults[0].Labels).To(HaveKeyWithValue("image", "blue"))
})
+ It("podman commit container with --config flag", func() {
+ test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"})
+ test.WaitWithDefaultTimeout()
+ Expect(test).Should(ExitCleanly())
+ Expect(podmanTest.NumberOfContainers()).To(Equal(1))
+
+ configFile, err := os.CreateTemp(podmanTest.TempDir, "")
+ Expect(err).Should(Succeed())
+ _, err = configFile.WriteString(`{"Labels":{"image":"green"}}`)
+ Expect(err).Should(Succeed())
+ configFile.Close()
+
+ session := podmanTest.Podman([]string{"commit", "-q", "--config", configFile.Name(), "test1", "foobar.com/test1-image:latest"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(ExitCleanly())
+
+ check := podmanTest.Podman([]string{"inspect", "foobar.com/test1-image:latest"})
+ check.WaitWithDefaultTimeout()
+ inspectResults := check.InspectImageJSON()
+ Expect(inspectResults[0].Labels).To(HaveKeyWithValue("image", "green"))
+ })
+
+ It("podman commit container with --config pointing to trash", func() {
+ test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"})
+ test.WaitWithDefaultTimeout()
+ Expect(test).Should(ExitCleanly())
+ Expect(podmanTest.NumberOfContainers()).To(Equal(1))
+
+ configFile, err := os.CreateTemp(podmanTest.TempDir, "")
+ Expect(err).Should(Succeed())
+ _, err = configFile.WriteString("this is not valid JSON\n")
+ Expect(err).Should(Succeed())
+ configFile.Close()
+
+ session := podmanTest.Podman([]string{"commit", "-q", "--config", configFile.Name(), "test1", "foobar.com/test1-image:latest"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Not(ExitCleanly()))
+ })
+
It("podman commit container with --squash", func() {
test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"})
test.WaitWithDefaultTimeout()