mirror of
https://github.com/containers/podman.git
synced 2025-05-17 15:18:43 +08:00

Currently in Docker if you commit with --change 'CMD a b c' The command that gets added is [/bin/sh -c "a b c"] If you commit --change 'CMD ["a","b","c"]' You get [a b c] This patch set makes podman match this behaviour. Similar change required for Entrypoint. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
242 lines
6.8 KiB
Go
242 lines
6.8 KiB
Go
package libpod
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/containers/buildah"
|
|
"github.com/containers/buildah/util"
|
|
is "github.com/containers/image/storage"
|
|
"github.com/containers/libpod/libpod/events"
|
|
"github.com/containers/libpod/libpod/image"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ContainerCommitOptions is a struct used to commit a container to an image
|
|
// It uses buildah's CommitOptions as a base. Long-term we might wish to
|
|
// add these to the buildah struct once buildah is more integrated with
|
|
//libpod
|
|
type ContainerCommitOptions struct {
|
|
buildah.CommitOptions
|
|
Pause bool
|
|
IncludeVolumes bool
|
|
Author string
|
|
Message string
|
|
Changes []string
|
|
}
|
|
|
|
// ChangeCmds is the list of valid Changes commands to passed to the Commit call
|
|
var ChangeCmds = []string{"CMD", "ENTRYPOINT", "ENV", "EXPOSE", "LABEL", "ONBUILD", "STOPSIGNAL", "USER", "VOLUME", "WORKDIR"}
|
|
|
|
// Commit commits the changes between a container and its image, creating a new
|
|
// image
|
|
func (c *Container) Commit(ctx context.Context, destImage string, options ContainerCommitOptions) (*image.Image, error) {
|
|
var (
|
|
isEnvCleared, isLabelCleared, isExposeCleared, isVolumeCleared bool
|
|
)
|
|
|
|
if c.config.Rootfs != "" {
|
|
return nil, errors.Errorf("cannot commit a container that uses an exploded rootfs")
|
|
}
|
|
|
|
if !c.batched {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
if err := c.syncContainer(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if c.state.State == ContainerStateRunning && options.Pause {
|
|
if err := c.runtime.ociRuntime.pauseContainer(c); err != nil {
|
|
return nil, errors.Wrapf(err, "error pausing container %q", c.ID())
|
|
}
|
|
defer func() {
|
|
if err := c.runtime.ociRuntime.unpauseContainer(c); err != nil {
|
|
logrus.Errorf("error unpausing container %q: %v", c.ID(), err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
sc := image.GetSystemContext(options.SignaturePolicyPath, "", false)
|
|
builderOptions := buildah.ImportOptions{
|
|
Container: c.ID(),
|
|
SignaturePolicyPath: options.SignaturePolicyPath,
|
|
}
|
|
commitOptions := buildah.CommitOptions{
|
|
SignaturePolicyPath: options.SignaturePolicyPath,
|
|
ReportWriter: options.ReportWriter,
|
|
SystemContext: sc,
|
|
PreferredManifestType: options.PreferredManifestType,
|
|
}
|
|
importBuilder, err := buildah.ImportBuilder(ctx, c.runtime.store, builderOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.Author != "" {
|
|
importBuilder.SetMaintainer(options.Author)
|
|
}
|
|
if options.Message != "" {
|
|
importBuilder.SetComment(options.Message)
|
|
}
|
|
|
|
// We need to take meta we find in the current container and
|
|
// add it to the resulting image.
|
|
|
|
// Entrypoint - always set this first or cmd will get wiped out
|
|
importBuilder.SetEntrypoint(c.config.Entrypoint)
|
|
|
|
// Cmd
|
|
importBuilder.SetCmd(c.config.Command)
|
|
|
|
// Env
|
|
// TODO - this includes all the default environment vars as well
|
|
// Should we store the ENV we actually want in the spec separately?
|
|
if c.config.Spec.Process != nil {
|
|
for _, e := range c.config.Spec.Process.Env {
|
|
splitEnv := strings.Split(e, "=")
|
|
importBuilder.SetEnv(splitEnv[0], splitEnv[1])
|
|
}
|
|
}
|
|
// Expose ports
|
|
for _, p := range c.config.PortMappings {
|
|
importBuilder.SetPort(fmt.Sprintf("%d", p.ContainerPort))
|
|
}
|
|
// Labels
|
|
for k, v := range c.Labels() {
|
|
importBuilder.SetLabel(k, v)
|
|
}
|
|
// No stop signal
|
|
// User
|
|
importBuilder.SetUser(c.User())
|
|
// Volumes
|
|
if options.IncludeVolumes {
|
|
for _, v := range c.config.UserVolumes {
|
|
if v != "" {
|
|
importBuilder.AddVolume(v)
|
|
}
|
|
}
|
|
}
|
|
// Workdir
|
|
importBuilder.SetWorkDir(c.Spec().Process.Cwd)
|
|
|
|
genCmd := func(cmd string) []string {
|
|
trim := func(cmd []string) []string {
|
|
if len(cmd) == 0 {
|
|
return cmd
|
|
}
|
|
|
|
retCmd := []string{}
|
|
for _, c := range cmd {
|
|
if len(c) >= 2 {
|
|
if c[0] == '"' && c[len(c)-1] == '"' {
|
|
retCmd = append(retCmd, c[1:len(c)-1])
|
|
continue
|
|
}
|
|
}
|
|
retCmd = append(retCmd, c)
|
|
}
|
|
return retCmd
|
|
}
|
|
if strings.HasPrefix(cmd, "[") {
|
|
cmd = strings.TrimPrefix(cmd, "[")
|
|
cmd = strings.TrimSuffix(cmd, "]")
|
|
return trim(strings.Split(cmd, ","))
|
|
}
|
|
return []string{"/bin/sh", "-c", cmd}
|
|
}
|
|
// Process user changes
|
|
for _, change := range options.Changes {
|
|
splitChange := strings.SplitN(change, "=", 2)
|
|
if len(splitChange) != 2 {
|
|
splitChange = strings.SplitN(change, " ", 2)
|
|
if len(splitChange) < 2 {
|
|
return nil, errors.Errorf("invalid change %s format", change)
|
|
}
|
|
}
|
|
|
|
switch strings.ToUpper(splitChange[0]) {
|
|
case "CMD":
|
|
importBuilder.SetCmd(genCmd(splitChange[1]))
|
|
case "ENTRYPOINT":
|
|
importBuilder.SetEntrypoint(genCmd(splitChange[1]))
|
|
case "ENV":
|
|
change := strings.Split(splitChange[1], " ")
|
|
name := change[0]
|
|
val := ""
|
|
if len(change) < 2 {
|
|
change = strings.Split(change[0], "=")
|
|
}
|
|
if len(change) < 2 {
|
|
var ok bool
|
|
val, ok = os.LookupEnv(name)
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid env variable %q: not defined in your environment", name)
|
|
}
|
|
} else {
|
|
val = strings.Join(change[1:], " ")
|
|
}
|
|
if !isEnvCleared { // Multiple values are valid, only clear once.
|
|
importBuilder.ClearEnv()
|
|
isEnvCleared = true
|
|
}
|
|
importBuilder.SetEnv(name, val)
|
|
case "EXPOSE":
|
|
if !isExposeCleared { // Multiple values are valid, only clear once
|
|
importBuilder.ClearPorts()
|
|
isExposeCleared = true
|
|
}
|
|
importBuilder.SetPort(splitChange[1])
|
|
case "LABEL":
|
|
change := strings.Split(splitChange[1], " ")
|
|
if len(change) < 2 {
|
|
change = strings.Split(change[0], "=")
|
|
}
|
|
if len(change) < 2 {
|
|
return nil, errors.Errorf("invalid label %s format, requires to NAME=VAL", splitChange[1])
|
|
}
|
|
if !isLabelCleared { // multiple values are valid, only clear once
|
|
importBuilder.ClearLabels()
|
|
isLabelCleared = true
|
|
}
|
|
importBuilder.SetLabel(change[0], strings.Join(change[1:], " "))
|
|
case "ONBUILD":
|
|
importBuilder.SetOnBuild(splitChange[1])
|
|
case "STOPSIGNAL":
|
|
// No Set StopSignal
|
|
case "USER":
|
|
importBuilder.SetUser(splitChange[1])
|
|
case "VOLUME":
|
|
if !isVolumeCleared { // multiple values are valid, only clear once
|
|
importBuilder.ClearVolumes()
|
|
isVolumeCleared = true
|
|
}
|
|
importBuilder.AddVolume(splitChange[1])
|
|
case "WORKDIR":
|
|
importBuilder.SetWorkDir(splitChange[1])
|
|
}
|
|
}
|
|
candidates, _, _, err := util.ResolveName(destImage, "", sc, c.runtime.store)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error resolving name %q", destImage)
|
|
}
|
|
if len(candidates) == 0 {
|
|
return nil, errors.Errorf("error parsing target image name %q", destImage)
|
|
}
|
|
imageRef, err := is.Transport.ParseStoreReference(c.runtime.store, candidates[0])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing target image name %q", destImage)
|
|
}
|
|
id, _, _, err := importBuilder.Commit(ctx, imageRef, commitOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer c.newContainerEvent(events.Commit)
|
|
return c.runtime.imageRuntime.NewFromLocal(id)
|
|
}
|