container runlabel

Implement container runlabel for v2.  Local client only.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2020-05-07 15:57:06 +02:00
parent 7cd2e35203
commit 61828cf480
6 changed files with 403 additions and 1 deletions

View File

@ -0,0 +1,80 @@
package containers
import (
"context"
"os"
"github.com/containers/common/pkg/auth"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/cmd/podman/registry"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// runlabelOptionsWrapper allows for combining API-only with CLI-only options
// and to convert between them.
type runlabelOptionsWrapper struct {
entities.ContainerRunlabelOptions
TLSVerifyCLI bool
}
var (
runlabelOptions = runlabelOptionsWrapper{}
runlabelDescription = "Executes a command as described by a container image label."
runlabelCommand = &cobra.Command{
Use: "runlabel [flags] LABEL IMAGE [ARG...]",
Short: "Execute the command described by an image label",
Long: runlabelDescription,
RunE: runlabel,
Args: cobra.MinimumNArgs(2),
Example: `podman container runlabel run imageID
podman container runlabel --pull install imageID arg1 arg2
podman container runlabel --display run myImage`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode},
Command: runlabelCommand,
Parent: containerCmd,
})
flags := rmCommand.Flags()
flags.StringVar(&runlabelOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
flags.StringVar(&runlabelOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys")
flags.StringVar(&runlabelOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
flags.BoolVar(&runlabelOptions.Display, "display", false, "Preview the command that the label would run")
flags.StringVarP(&runlabelOptions.Name, "name", "n", "", "Assign a name to the container")
flags.StringVar(&runlabelOptions.Optional1, "opt1", "", "Optional parameter to pass for install")
flags.StringVar(&runlabelOptions.Optional2, "opt2", "", "Optional parameter to pass for install")
flags.StringVar(&runlabelOptions.Optional3, "opt3", "", "Optional parameter to pass for install")
flags.BoolP("pull", "p", false, "Pull the image if it does not exist locally prior to executing the label contents")
flags.BoolVarP(&runlabelOptions.Quiet, "quiet", "q", false, "Suppress output information when installing images")
flags.BoolVar(&runlabelOptions.Replace, "replace", false, "Replace existing container with a new one from the image")
flags.StringVar(&runlabelOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)")
flags.BoolVar(&runlabelOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
// Hide the optional flags.
_ = flags.MarkHidden("opt1")
_ = flags.MarkHidden("opt2")
_ = flags.MarkHidden("opt3")
if err := flags.MarkDeprecated("pull", "podman will pull if not found in local storage"); err != nil {
logrus.Error("unable to mark pull flag deprecated")
}
}
func runlabel(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("tls-verify") {
runlabelOptions.SkipTLSVerify = types.NewOptionalBool(!runlabelOptions.TLSVerifyCLI)
}
if runlabelOptions.Authfile != "" {
if _, err := os.Stat(runlabelOptions.Authfile); err != nil {
return errors.Wrapf(err, "error getting authfile %s", runlabelOptions.Authfile)
}
}
return registry.ContainerEngine().ContainerRunlabel(context.Background(), args[0], args[1], args[2:], runlabelOptions.ContainerRunlabelOptions)
}

View File

@ -6,11 +6,49 @@ import (
"os"
"time"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/specgen"
"github.com/cri-o/ocicni/pkg/ocicni"
)
// ContainerRunlabelOptions are the options to execute container-runlabel.
type ContainerRunlabelOptions struct {
// Authfile - path to an authentication file.
Authfile string
// CertDir - path to a directory containing TLS certifications and
// keys.
CertDir string
// Credentials - `user:password` to use when pulling an image.
Credentials string
// Display - do not execute but print the command.
Display bool
// Replace - replace an existing container with a new one from the
// image.
Replace bool
// Name - use this name when executing the runlabel container.
Name string
// Optional1 - fist optional parameter for install.
Optional1 string
// Optional2 - second optional parameter for install.
Optional2 string
// Optional3 - third optional parameter for install.
Optional3 string
// Pull - pull the specified image if it's not in the local storage.
Pull bool
// Quiet - suppress output when pulling images.
Quiet bool
// SignaturePolicy - path to a signature-policy file.
SignaturePolicy string
// SkipTLSVerify - skip HTTPS and certificate verifications when
// contacting registries.
SkipTLSVerify types.OptionalBool
}
// ContainerRunlabelReport contains the results from executing container-runlabel.
type ContainerRunlabelReport struct {
}
type WaitOptions struct {
Condition define.ContainerStatus
Interval time.Duration

View File

@ -34,6 +34,7 @@ type ContainerEngine interface {
ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error)
ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error)
ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error)
ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error
ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error)
ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) error
ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error)

View File

@ -0,0 +1,280 @@
package abi
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/domain/entities"
envLib "github.com/containers/libpod/pkg/env"
"github.com/containers/libpod/pkg/util"
"github.com/containers/libpod/utils"
"github.com/google/shlex"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func (ic *ContainerEngine) ContainerRunlabel(ctx context.Context, label string, imageRef string, args []string, options entities.ContainerRunlabelOptions) error {
// First, get the image and pull it if needed.
img, err := ic.runlabelImage(ctx, label, imageRef, options)
if err != nil {
return err
}
// Extract the runlabel from the image.
runlabel, err := img.GetLabel(ctx, label)
if err != nil {
return err
}
cmd, env, err := generateRunlabelCommand(runlabel, img, args, options)
if err != nil {
return err
}
stdErr := os.Stderr
stdOut := os.Stdout
stdIn := os.Stdin
if options.Quiet {
stdErr = nil
stdOut = nil
stdIn = nil
}
// If container already exists && --replace given -- Nuke it
if options.Replace {
for i, entry := range cmd {
if entry == "--name" {
name := cmd[i+1]
ctr, err := ic.Libpod.LookupContainer(name)
if err != nil {
if errors.Cause(err) != define.ErrNoSuchCtr {
logrus.Debugf("Error occurred searching for container %s: %s", name, err.Error())
return err
}
} else {
logrus.Debugf("Runlabel --replace option given. Container %s will be deleted. The new container will be named %s", ctr.ID(), name)
if err := ic.Libpod.RemoveContainer(ctx, ctr, true, false); err != nil {
return err
}
}
break
}
}
}
return utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...)
}
// runlabelImage returns an image based on the specified image AND options.
func (ic *ContainerEngine) runlabelImage(ctx context.Context, label string, imageRef string, options entities.ContainerRunlabelOptions) (*image.Image, error) {
// First, look up the image locally. If we get an error and requested
// to pull, fallthrough and pull it.
img, err := ic.Libpod.ImageRuntime().NewFromLocal(imageRef)
switch {
case err == nil:
return img, nil
case !options.Pull:
return nil, err
default:
// Fallthrough and pull!
}
// Parse credentials if specified.
var credentials *types.DockerAuthConfig
if options.Credentials != "" {
credentials, err = util.ParseRegistryCreds(options.Credentials)
if err != nil {
return nil, err
}
}
// Suppress pull progress bars if requested.
pullOutput := os.Stdout
if options.Quiet {
pullOutput = nil // c/image/copy takes care of the rest
}
// Pull the image.
dockerRegistryOptions := image.DockerRegistryOptions{
DockerCertPath: options.CertDir,
DockerInsecureSkipTLSVerify: options.SkipTLSVerify,
DockerRegistryCreds: credentials,
}
return ic.Libpod.ImageRuntime().New(ctx, imageRef, options.SignaturePolicy, options.Authfile, pullOutput, &dockerRegistryOptions, image.SigningOptions{}, &label, util.PullImageMissing)
}
// generateRunlabelCommand generates the to-be-executed command as a string
// slice along with a base environment.
func generateRunlabelCommand(runlabel string, img *image.Image, args []string, options entities.ContainerRunlabelOptions) ([]string, []string, error) {
var (
err error
name, imageName string
globalOpts string
cmd, env []string
)
// TODO: How do we get global opts as done in v1?
// Extract the imageName (or ID).
imgNames := img.Names()
if len(imgNames) == 0 {
imageName = img.ID()
} else {
imageName = imgNames[0]
}
// Use the user-specified name or extract one from the image.
if options.Name != "" {
name = options.Name
} else {
name, err = image.GetImageBaseName(imageName)
if err != nil {
return nil, nil, err
}
}
// Append the user-specified arguments to the runlabel (command).
if len(args) > 0 {
runlabel = fmt.Sprintf("%s %s", runlabel, strings.Join(args, " "))
}
cmd, err = generateCommand(runlabel, imageName, name, globalOpts)
if err != nil {
return nil, nil, err
}
env = generateRunEnvironment(name, imageName, options)
env = append(env, "PODMAN_RUNLABEL_NESTED=1")
envmap, err := envLib.ParseSlice(env)
if err != nil {
return nil, nil, err
}
envmapper := func(k string) string {
switch k {
case "OPT1":
return envmap["OPT1"]
case "OPT2":
return envmap["OPT2"]
case "OPT3":
return envmap["OPT3"]
case "PWD":
// I would prefer to use os.getenv but it appears PWD is not in the os env list.
d, err := os.Getwd()
if err != nil {
logrus.Error("unable to determine current working directory")
return ""
}
return d
}
return ""
}
newS := os.Expand(strings.Join(cmd, " "), envmapper)
cmd, err = shlex.Split(newS)
if err != nil {
return nil, nil, err
}
return cmd, env, nil
}
// generateCommand takes a label (string) and converts it to an executable command
func generateCommand(command, imageName, name, globalOpts string) ([]string, error) {
var (
newCommand []string
)
if name == "" {
name = imageName
}
cmd, err := shlex.Split(command)
if err != nil {
return nil, err
}
prog, err := substituteCommand(cmd[0])
if err != nil {
return nil, err
}
newCommand = append(newCommand, prog)
for _, arg := range cmd[1:] {
var newArg string
switch arg {
case "IMAGE":
newArg = imageName
case "$IMAGE":
newArg = imageName
case "IMAGE=IMAGE":
newArg = fmt.Sprintf("IMAGE=%s", imageName)
case "IMAGE=$IMAGE":
newArg = fmt.Sprintf("IMAGE=%s", imageName)
case "NAME":
newArg = name
case "NAME=NAME":
newArg = fmt.Sprintf("NAME=%s", name)
case "NAME=$NAME":
newArg = fmt.Sprintf("NAME=%s", name)
case "$NAME":
newArg = name
case "$GLOBAL_OPTS":
newArg = globalOpts
default:
newArg = arg
}
newCommand = append(newCommand, newArg)
}
return newCommand, nil
}
// GenerateRunEnvironment merges the current environment variables with optional
// environment variables provided by the user
func generateRunEnvironment(name, imageName string, options entities.ContainerRunlabelOptions) []string {
newEnv := os.Environ()
if options.Optional1 != "" {
newEnv = append(newEnv, fmt.Sprintf("OPT1=%s", options.Optional1))
}
if options.Optional2 != "" {
newEnv = append(newEnv, fmt.Sprintf("OPT2=%s", options.Optional2))
}
if options.Optional3 != "" {
newEnv = append(newEnv, fmt.Sprintf("OPT3=%s", options.Optional3))
}
return newEnv
}
func substituteCommand(cmd string) (string, error) {
var (
newCommand string
)
// Replace cmd with "/proc/self/exe" if "podman" or "docker" is being
// used. If "/usr/bin/docker" is provided, we also sub in podman.
// Otherwise, leave the command unchanged.
if cmd == "podman" || filepath.Base(cmd) == "docker" {
newCommand = "/proc/self/exe"
} else {
newCommand = cmd
}
// If cmd is an absolute or relative path, check if the file exists.
// Throw an error if it doesn't exist.
if strings.Contains(newCommand, "/") || strings.HasPrefix(newCommand, ".") {
res, err := filepath.Abs(newCommand)
if err != nil {
return "", err
}
if _, err := os.Stat(res); !os.IsNotExist(err) {
return res, nil
} else if err != nil {
return "", err
}
}
return newCommand, nil
}

View File

@ -14,6 +14,10 @@ import (
"github.com/pkg/errors"
)
func (ic *ContainerEngine) ContainerRunlabel(ctx context.Context, label string, image string, args []string, options entities.ContainerRunlabelOptions) error {
return errors.New("not implemented")
}
func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) {
exists, err := containers.Exists(ic.ClientCxt, nameOrId)
return &entities.BoolReport{Value: exists}, err

View File

@ -31,7 +31,6 @@ var _ = Describe("podman container runlabel", func() {
)
BeforeEach(func() {
Skip(v2fail)
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)