diff options
Diffstat (limited to 'pkg')
172 files changed, 8455 insertions, 1787 deletions
diff --git a/pkg/adapter/autoupdate.go b/pkg/adapter/autoupdate.go new file mode 100644 index 000000000..01f7a29c5 --- /dev/null +++ b/pkg/adapter/autoupdate.go @@ -0,0 +1,11 @@ +// +build !remoteclient + +package adapter + +import ( + "github.com/containers/libpod/pkg/autoupdate" +) + +func (r *LocalRuntime) AutoUpdate() ([]string, []error) { + return autoupdate.AutoUpdate(r.Runtime) +} diff --git a/pkg/adapter/autoupdate_remote.go b/pkg/adapter/autoupdate_remote.go new file mode 100644 index 000000000..a2a82d0d4 --- /dev/null +++ b/pkg/adapter/autoupdate_remote.go @@ -0,0 +1,11 @@ +// +build remoteclient + +package adapter + +import ( + "github.com/containers/libpod/libpod/define" +) + +func (r *LocalRuntime) AutoUpdate() ([]string, []error) { + return nil, []error{define.ErrNotImplemented} +} diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/adapter/checkpoint_restore.go index 7f80b782a..a5b74013b 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/adapter/checkpoint_restore.go @@ -114,7 +114,7 @@ func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri return nil, err } - _, err = runtime.ImageRuntime().New(ctx, config.RootfsImageName, rtc.SignaturePolicyPath, "", writer, nil, image.SigningOptions{}, nil, util.PullImageMissing) + _, err = runtime.ImageRuntime().New(ctx, config.RootfsImageName, rtc.Engine.SignaturePolicyPath, "", writer, nil, image.SigningOptions{}, nil, util.PullImageMissing) if err != nil { return nil, err } diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 08e19edb8..a2f73307b 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -16,16 +16,17 @@ import ( "time" "github.com/containers/buildah" + cfg "github.com/containers/common/pkg/config" "github.com/containers/image/v5/manifest" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/shared/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" + envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/storage" "github.com/pkg/errors" @@ -380,11 +381,11 @@ func (r *LocalRuntime) selectDetachKeys(flagValue string) (string, error) { if err != nil { return "", errors.Wrapf(err, "unable to retrieve runtime config") } - if config.DetachKeys != "" { - return config.DetachKeys, nil + if config.Engine.DetachKeys != "" { + return config.Engine.DetachKeys, nil } - return define.DefaultDetachKeys, nil + return cfg.DefaultDetachKeys, nil } // Run a libpod container @@ -987,9 +988,20 @@ func (r *LocalRuntime) ExecContainer(ctx context.Context, cli *cliconfig.ExecVal // Validate given environment variables env := map[string]string{} - if err := parse.ReadKVStrings(env, cli.EnvFile, cli.Env); err != nil { - return ec, errors.Wrapf(err, "unable to process environment variables") + if len(cli.EnvFile) > 0 { + for _, f := range cli.EnvFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return ec, err + } + env = envLib.Join(env, fileEnv) + } } + cliEnv, err := envLib.ParseSlice(cli.Env) + if err != nil { + return ec, errors.Wrap(err, "error parsing environment variables") + } + env = envLib.Join(env, cliEnv) streams := new(libpod.AttachStreams) streams.OutputStream = os.Stdout @@ -1046,7 +1058,8 @@ func (r *LocalRuntime) Prune(ctx context.Context, maxWorkers int, filters []stri if c.PodID() != "" { return false } - if state == define.ContainerStateStopped || state == define.ContainerStateExited { + if state == define.ContainerStateStopped || state == define.ContainerStateExited || + state == define.ContainerStateCreated || state == define.ContainerStateConfigured { return true } return false @@ -1101,6 +1114,15 @@ func (r *LocalRuntime) CleanupContainers(ctx context.Context, cli *cliconfig.Cle } else { failures[ctr.ID()] = err } + + if cli.RemoveImage { + _, imageName := ctr.Image() + if err := removeContainerImage(ctx, ctr, r); err != nil { + failures[imageName] = err + } else { + ok = append(ok, imageName) + } + } } return ok, failures, nil } @@ -1120,6 +1142,16 @@ func cleanupContainer(ctx context.Context, ctr *libpod.Container, runtime *Local return nil } +func removeContainerImage(ctx context.Context, ctr *libpod.Container, runtime *LocalRuntime) error { + _, imageName := ctr.Image() + ctrImage, err := runtime.NewImageFromLocal(imageName) + if err != nil { + return err + } + _, err = runtime.RemoveImage(ctx, ctrImage, false) + return err +} + // Port displays port information about existing containers func (r *LocalRuntime) Port(c *cliconfig.PortValues) ([]*Container, error) { var ( @@ -1338,9 +1370,9 @@ func (r *LocalRuntime) Commit(ctx context.Context, c *cliconfig.CommitValues, co return "", err } - sc := image.GetSystemContext(rtc.SignaturePolicyPath, "", false) + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) coptions := buildah.CommitOptions{ - SignaturePolicyPath: rtc.SignaturePolicyPath, + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, ReportWriter: writer, SystemContext: sc, PreferredManifestType: mimeType, diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index 60ee3cb2d..46db7ebe8 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -15,11 +15,11 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/shared/parse" iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/logs" + envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/varlinkapi/virtwriter" "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/docker/pkg/term" @@ -32,12 +32,12 @@ import ( ) // Inspect returns an inspect struct from varlink -func (c *Container) Inspect(size bool) (*libpod.InspectContainerData, error) { +func (c *Container) Inspect(size bool) (*define.InspectContainerData, error) { reply, err := iopodman.ContainerInspectData().Call(c.Runtime.Conn, c.ID(), size) if err != nil { return nil, err } - data := libpod.InspectContainerData{} + data := define.InspectContainerData{} if err := json.Unmarshal([]byte(reply), &data); err != nil { return nil, err } @@ -1025,16 +1025,11 @@ func (r *LocalRuntime) ExecContainer(ctx context.Context, cli *cliconfig.ExecVal ) // default invalid command exit code // Validate given environment variables - env := map[string]string{} - if err := parse.ReadKVStrings(env, []string{}, cli.Env); err != nil { - return -1, errors.Wrapf(err, "Exec unable to process environment variables") - } - - // Build env slice of key=value strings for Exec - envs := []string{} - for k, v := range env { - envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + cliEnv, err := envLib.ParseSlice(cli.Env) + if err != nil { + return 0, errors.Wrap(err, "error parsing environment variables") } + envs := envLib.Slice(cliEnv) resize := make(chan remotecommand.TerminalSize, 5) haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) diff --git a/pkg/adapter/network.go b/pkg/adapter/network.go index c5bd91534..577ffe19f 100644 --- a/pkg/adapter/network.go +++ b/pkg/adapter/network.go @@ -23,9 +23,9 @@ func getCNIConfDir(r *LocalRuntime) (string, error) { if err != nil { return "", err } - configPath := config.CNIConfigDir + configPath := config.Network.NetworkConfigDir - if len(config.CNIConfigDir) < 1 { + if len(config.Network.NetworkConfigDir) < 1 { configPath = network.CNIConfigDir } return configPath, nil @@ -209,8 +209,9 @@ func (r *LocalRuntime) NetworkCreateBridge(cli *cliconfig.NetworkCreateValues) ( bridge := network.NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) plugins = append(plugins, bridge) plugins = append(plugins, network.NewPortMapPlugin()) + plugins = append(plugins, network.NewFirewallPlugin()) // if we find the dnsname plugin, we add configuration for it - if network.HasDNSNamePlugin(runtimeConfig.CNIPluginDir) && !cli.DisableDNS { + if network.HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !cli.DisableDNS { // Note: in the future we might like to allow for dynamic domain names plugins = append(plugins, network.NewDNSNamePlugin(network.DefaultPodmanDomainName)) } diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index 0d9fa7210..102eabd8b 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -22,6 +22,7 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/adapter/shortcuts" ann "github.com/containers/libpod/pkg/annotations" + envLib "github.com/containers/libpod/pkg/env" ns "github.com/containers/libpod/pkg/namespaces" createconfig "github.com/containers/libpod/pkg/spec" "github.com/containers/libpod/pkg/util" @@ -121,19 +122,31 @@ func (r *LocalRuntime) GetLatestPod() (*Pod, error) { return &pod, err } +// GetPodsWithFilters gets the filtered list of pods based on the filter parameters provided. +func (r *LocalRuntime) GetPodsWithFilters(filters string) ([]*Pod, error) { + pods, err := shared.GetPodsWithFilters(r.Runtime, filters) + if err != nil { + return nil, err + } + return r.podstoAdapterPods(pods) +} + +func (r *LocalRuntime) podstoAdapterPods(pod []*libpod.Pod) ([]*Pod, error) { + var pods []*Pod + for _, i := range pod { + + pods = append(pods, &Pod{i}) + } + return pods, nil +} + // GetAllPods gets all pods and wraps it in an adapter pod func (r *LocalRuntime) GetAllPods() ([]*Pod, error) { - var pods []*Pod allPods, err := r.Runtime.GetAllPods() if err != nil { return nil, err } - for _, p := range allPods { - pod := Pod{} - pod.Pod = p - pods = append(pods, &pod) - } - return pods, nil + return r.podstoAdapterPods(allPods) } // LookupPod gets a pod by name or id and wraps it in an adapter pod @@ -755,6 +768,12 @@ func getPodPorts(containers []v1.Container) []ocicni.PortMapping { var infraPorts []ocicni.PortMapping for _, container := range containers { for _, p := range container.Ports { + if p.HostPort != 0 && p.ContainerPort == 0 { + p.ContainerPort = p.HostPort + } + if p.Protocol == "" { + p.Protocol = "tcp" + } portBinding := ocicni.PortMapping{ HostPort: p.HostPort, ContainerPort: p.ContainerPort, @@ -763,7 +782,12 @@ func getPodPorts(containers []v1.Container) []ocicni.PortMapping { if p.HostIP != "" { logrus.Debug("HostIP on port bindings is not supported") } - infraPorts = append(infraPorts, portBinding) + // only hostPort is utilized in podman context, all container ports + // are accessible inside the shared network namespace + if p.HostPort != 0 { + infraPorts = append(infraPorts, portBinding) + } + } } return infraPorts @@ -916,9 +940,6 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.User = userConfig containerConfig.Security = securityConfig - // Set default environment variables and incorporate data from image, if necessary - envs := shared.EnvVariablesFromData(imageData) - annotations := make(map[string]string) if infraID != "" { annotations[ann.SandboxID] = infraID @@ -927,6 +948,14 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.Annotations = annotations // Environment Variables + envs := map[string]string{} + if imageData != nil { + imageEnv, err := envLib.ParseSlice(imageData.Config.Env) + if err != nil { + return nil, errors.Wrap(err, "error parsing image environment variables") + } + envs = imageEnv + } for _, e := range containerYAML.Env { envs[e.Name] = e.Value } diff --git a/pkg/adapter/pods_remote.go b/pkg/adapter/pods_remote.go index 20f089628..6b8f22f15 100644 --- a/pkg/adapter/pods_remote.go +++ b/pkg/adapter/pods_remote.go @@ -10,7 +10,7 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/varlink" + iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/varlinkapi" @@ -208,6 +208,11 @@ func (r *LocalRuntime) GetAllPods() ([]*Pod, error) { return pods, nil } +// This is a empty implementation stating remoteclient not yet implemented +func (r *LocalRuntime) GetPodsWithFilters(filters string) ([]*Pod, error) { + return nil, define.ErrNotImplemented +} + // GetPodsByStatus returns a slice of pods filtered by a libpod status func (r *LocalRuntime) GetPodsByStatus(statuses []string) ([]*Pod, error) { podIDs, err := iopodman.GetPodsByStatus().Call(r.Conn, statuses) diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index dfe6b7f07..7a181e7e5 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -13,7 +13,6 @@ import ( "github.com/containers/buildah" "github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/pkg/formats" - "github.com/containers/buildah/pkg/parse" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/cliconfig" @@ -296,37 +295,13 @@ func libpodVolumeToVolume(volumes []*libpod.Volume) []*Volume { // Build is the wrapper to build images func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, options imagebuildah.BuildOptions, dockerfiles []string) (string, reference.Canonical, error) { - namespaceOptions, networkPolicy, err := parse.NamespaceOptions(c.PodmanCommand.Command) - if err != nil { - return "", nil, errors.Wrapf(err, "error parsing namespace-related options") - } - usernsOption, idmappingOptions, err := parse.IDMappingOptions(c.PodmanCommand.Command, options.Isolation) - if err != nil { - return "", nil, errors.Wrapf(err, "error parsing ID mapping options") - } - namespaceOptions.AddOrReplace(usernsOption...) - - systemContext, err := parse.SystemContextFromOptions(c.PodmanCommand.Command) - if err != nil { - return "", nil, errors.Wrapf(err, "error building system context") - } authfile := c.Authfile if len(c.Authfile) == 0 { authfile = os.Getenv("REGISTRY_AUTH_FILE") } - systemContext.AuthFilePath = authfile - commonOpts, err := parse.CommonBuildOptions(c.PodmanCommand.Command) - if err != nil { - return "", nil, err - } - - options.NamespaceOptions = namespaceOptions - options.ConfigureNetwork = networkPolicy - options.IDMappingOptions = idmappingOptions - options.CommonBuildOpts = commonOpts - options.SystemContext = systemContext + options.SystemContext.AuthFilePath = authfile if c.GlobalFlags.Runtime != "" { options.Runtime = c.GlobalFlags.Runtime @@ -347,7 +322,23 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti // PruneVolumes is a wrapper function for libpod PruneVolumes func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { - return r.Runtime.PruneVolumes(ctx) + var ( + vids []string + errs []error + ) + reports, err := r.Runtime.PruneVolumes(ctx) + if err != nil { + errs = append(errs, err) + return vids, errs + } + for k, v := range reports { + if v == nil { + vids = append(vids, k) + } else { + errs = append(errs, v) + } + } + return vids, errs } // SaveImage is a wrapper function for saving an image to the local filesystem diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index 220d4cf75..a616e6c7a 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -17,6 +17,7 @@ import ( "github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/pkg/formats" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" "github.com/containers/libpod/cmd/podman/cliconfig" @@ -113,15 +114,20 @@ func (r RemoteRuntime) DeferredShutdown(force bool) { } } -// RuntimeConfig is a bogus wrapper for compat with the libpod runtime -type RuntimeConfig struct { +// Containers is a bogus wrapper for compat with the libpod runtime +type ContainersConfig struct { // CGroupManager is the CGroup Manager to use // Valid values are "cgroupfs" and "systemd" CgroupManager string } +// RuntimeConfig is a bogus wrapper for compat with the libpod runtime +type RuntimeConfig struct { + Containers ContainersConfig +} + // Shutdown is a bogus wrapper for compat with the libpod runtime -func (r *RemoteRuntime) GetConfig() (*RuntimeConfig, error) { +func (r *RemoteRuntime) GetConfig() (*config.Config, error) { return nil, nil } @@ -201,8 +207,11 @@ func (r *LocalRuntime) GetRWImages() ([]*ContainerImage, error) { } func (r *LocalRuntime) GetFilteredImages(filters []string, rwOnly bool) ([]*ContainerImage, error) { + if len(filters) > 0 { + return nil, errors.Wrap(define.ErrNotImplemented, "filtering images is not supported on the remote client") + } var newImages []*ContainerImage - images, err := iopodman.ListImagesWithFilters().Call(r.Conn, filters) + images, err := iopodman.ListImages().Call(r.Conn) if err != nil { return nil, err } @@ -288,7 +297,8 @@ func (r *LocalRuntime) NewImageFromLocal(name string) (*ContainerImage, error) { // LoadFromArchiveReference creates an image from a local archive func (r *LocalRuntime) LoadFromArchiveReference(ctx context.Context, srcRef types.ImageReference, signaturePolicyPath string, writer io.Writer) ([]*ContainerImage, error) { var iid string - reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, srcRef.DockerReference().String()) + creds := iopodman.AuthConfig{} + reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, srcRef.DockerReference().String(), creds) if err != nil { return nil, err } @@ -320,7 +330,12 @@ func (r *LocalRuntime) New(ctx context.Context, name, signaturePolicyPath, authf if label != nil { return nil, errors.New("the remote client function does not support checking a remote image for a label") } - reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, name) + creds := iopodman.AuthConfig{} + if dockeroptions.DockerRegistryCreds != nil { + creds.Username = dockeroptions.DockerRegistryCreds.Username + creds.Password = dockeroptions.DockerRegistryCreds.Password + } + reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, name, creds) if err != nil { return nil, err } @@ -526,32 +541,40 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti Ulimit: options.CommonBuildOpts.Ulimit, Volume: options.CommonBuildOpts.Volumes, } - buildinfo := iopodman.BuildInfo{ - AdditionalTags: options.AdditionalTags, - Annotations: options.Annotations, - BuildArgs: options.Args, - BuildOptions: buildOptions, - CniConfigDir: options.CNIConfigDir, - CniPluginDir: options.CNIPluginPath, - Compression: string(options.Compression), - DefaultsMountFilePath: options.DefaultMountsFilePath, - Dockerfiles: dockerfiles, // Err: string(options.Err), + // Out: + // ReportWriter: + Architecture: options.Architecture, + AddCapabilities: options.AddCapabilities, + AdditionalTags: options.AdditionalTags, + Annotations: options.Annotations, + BuildArgs: options.Args, + BuildOptions: buildOptions, + CniConfigDir: options.CNIConfigDir, + CniPluginDir: options.CNIPluginPath, + Compression: string(options.Compression), + Devices: options.Devices, + DefaultsMountFilePath: options.DefaultMountsFilePath, + Dockerfiles: dockerfiles, + DropCapabilities: options.DropCapabilities, ForceRmIntermediateCtrs: options.ForceRmIntermediateCtrs, Iidfile: options.IIDFile, Label: options.Labels, Layers: options.Layers, - Nocache: options.NoCache, - // Out: + // NamespaceOptions: options.NamespaceOptions, + Nocache: options.NoCache, + Os: options.OS, Output: options.Output, OutputFormat: options.OutputFormat, PullPolicy: options.PullPolicy.String(), Quiet: options.Quiet, RemoteIntermediateCtrs: options.RemoveIntermediateCtrs, - // ReportWriter: - RuntimeArgs: options.RuntimeArgs, - Squash: options.Squash, + RuntimeArgs: options.RuntimeArgs, + SignBy: options.SignBy, + Squash: options.Squash, + Target: options.Target, + TransientMounts: options.TransientMounts, } // tar the file outputFile, err := ioutil.TempFile("", "varlink_tar_send") diff --git a/pkg/adapter/sigproxy_linux.go b/pkg/adapter/sigproxy_linux.go index 8295e4250..5695d0e42 100644 --- a/pkg/adapter/sigproxy_linux.go +++ b/pkg/adapter/sigproxy_linux.go @@ -20,7 +20,10 @@ func ProxySignals(ctr *libpod.Container) { for s := range sigBuffer { // Ignore SIGCHLD and SIGPIPE - these are mostly likely // intended for the podman command itself. - if s == syscall.SIGCHLD || s == syscall.SIGPIPE { + // SIGURG was added because of golang 1.14 and its preemptive changes + // causing more signals to "show up". + // https://github.com/containers/libpod/issues/5483 + if s == syscall.SIGCHLD || s == syscall.SIGPIPE || s == syscall.SIGURG { continue } diff --git a/pkg/adapter/terminal_linux.go b/pkg/adapter/terminal_linux.go index 3dc5864e2..ef5a6f926 100644 --- a/pkg/adapter/terminal_linux.go +++ b/pkg/adapter/terminal_linux.go @@ -16,7 +16,6 @@ import ( // ExecAttachCtr execs and attaches to a container func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, tty, privileged bool, env map[string]string, cmd []string, user, workDir string, streams *libpod.AttachStreams, preserveFDs uint, detachKeys string) (int, error) { resize := make(chan remotecommand.TerminalSize) - haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) // Check if we are attached to a terminal. If we are, generate resize @@ -33,7 +32,18 @@ func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, tty, privileged b } }() } - return ctr.Exec(tty, privileged, env, cmd, user, workDir, streams, preserveFDs, resize, detachKeys) + + execConfig := new(libpod.ExecConfig) + execConfig.Command = cmd + execConfig.Terminal = tty + execConfig.Privileged = privileged + execConfig.Environment = env + execConfig.User = user + execConfig.WorkDir = workDir + execConfig.DetachKeys = &detachKeys + execConfig.PreserveFDs = preserveFDs + + return ctr.Exec(execConfig, streams, resize) } // StartAttachCtr starts and (if required) attaches to a container diff --git a/pkg/adapter/terminal_unsupported.go b/pkg/adapter/terminal_unsupported.go new file mode 100644 index 000000000..3009f0a38 --- /dev/null +++ b/pkg/adapter/terminal_unsupported.go @@ -0,0 +1,23 @@ +// +build !linux + +package adapter + +import ( + "context" + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" +) + +// ExecAttachCtr execs and attaches to a container +func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, tty, privileged bool, env map[string]string, cmd []string, user, workDir string, streams *libpod.AttachStreams, preserveFDs uint, detachKeys string) (int, error) { + return -1, define.ErrNotImplemented +} + +// StartAttachCtr starts and (if required) attaches to a container +// if you change the signature of this function from os.File to io.Writer, it will trigger a downstream +// error. we may need to just lint disable this one. +func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { //nolint-interfacer + return define.ErrNotImplemented +} diff --git a/pkg/api/handlers/compat/container_start.go b/pkg/api/handlers/compat/container_start.go new file mode 100644 index 000000000..d26ef2c82 --- /dev/null +++ b/pkg/api/handlers/compat/container_start.go @@ -0,0 +1,60 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func StopContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + // /{version}/containers/(name)/stop + query := struct { + Timeout int `schema:"t"` + }{ + // override any golang type defaults + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := con.State() + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "unable to get state for Container %s", name)) + return + } + // If the Container is stopped already, send a 304 + if state == define.ContainerStateStopped || state == define.ContainerStateExited { + utils.WriteResponse(w, http.StatusNotModified, "") + return + } + + var stopError error + if query.Timeout > 0 { + stopError = con.StopWithTimeout(uint(query.Timeout)) + } else { + stopError = con.Stop() + } + if stopError != nil { + utils.InternalServerError(w, errors.Wrapf(stopError, "failed to stop %s", name)) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/generic/containers.go b/pkg/api/handlers/compat/containers.go index b16b87e73..2ce113d30 100644 --- a/pkg/api/handlers/generic/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/binary" @@ -10,11 +10,12 @@ import ( "time" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/util" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -35,12 +36,26 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - if query.Link { + + if query.Link && !utils.IsLibpodRequest(r) { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, utils.ErrLinkNotSupport) return } - utils.RemoveContainer(w, r, query.Force, query.Vols) + + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + if err := runtime.RemoveContainer(r.Context(), con, query.Force, query.Vols); err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") } func ListContainers(w http.ResponseWriter, r *http.Request) { @@ -58,6 +73,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { }{ // override any golang type defaults } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return @@ -71,7 +87,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - if _, found := mux.Vars(r)["limit"]; found { + if _, found := r.URL.Query()["limit"]; found && query.Limit != -1 { last := query.Limit if len(containers) > last { containers = containers[len(containers)-last:] @@ -86,7 +102,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { var list = make([]*handlers.Container, len(containers)) for i, ctnr := range containers { - api, err := handlers.LibpodToContainer(ctnr, infoData) + api, err := handlers.LibpodToContainer(ctnr, infoData, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -98,6 +114,17 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { func GetContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Size bool `schema:"size"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } name := utils.GetName(r) ctnr, err := runtime.LookupContainer(name) @@ -105,7 +132,7 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { utils.ContainerNotFound(w, name, err) return } - api, err := handlers.LibpodToContainerJSON(ctnr) + api, err := handlers.LibpodToContainerJSON(ctnr, query.Size) if err != nil { utils.InternalServerError(w, err) return @@ -115,18 +142,57 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { func KillContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/kill - con, err := utils.KillContainer(w, r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Signal string `schema:"signal"` + }{ + Signal: "KILL", + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + sig, err := signal.ParseSignalNameOrNumber(query.Signal) + if err != nil { + utils.InternalServerError(w, err) + return + } + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := con.State() if err != nil { + utils.InternalServerError(w, err) return } - // the kill behavior for docker differs from podman in that they appear to wait - // for the Container to croak so the exit code is accurate immediately after the - // kill is sent. libpod does not. but we can add a wait here only for the docker - // side of things and mimic that behavior - if _, err = con.Wait(); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "failed to wait for Container %s", con.ID())) + + // If the Container is stopped already, send a 409 + if state == define.ContainerStateStopped || state == define.ContainerStateExited { + utils.Error(w, fmt.Sprintf("Container %s is not running", name), http.StatusConflict, errors.New(fmt.Sprintf("Cannot kill Container %s, it is not running", name))) return } + + err = con.Kill(uint(sig)) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "unable to kill Container %s", name)) + } + + if utils.IsLibpodRequest(r) { + // the kill behavior for docker differs from podman in that they appear to wait + // for the Container to croak so the exit code is accurate immediately after the + // kill is sent. libpod does not. but we can add a wait here only for the docker + // side of things and mimic that behavior + if _, err = con.Wait(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "failed to wait for Container %s", con.ID())) + return + } + } // Success utils.WriteResponse(w, http.StatusNoContent, "") } @@ -136,7 +202,7 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/wait exitCode, err := utils.WaitContainer(w, r) if err != nil { - msg = err.Error() + return } utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{ StatusCode: int(exitCode), @@ -191,7 +257,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { } var since time.Time - if _, found := mux.Vars(r)["since"]; found { + if _, found := r.URL.Query()["since"]; found { since, err = util.ParseInputTime(query.Since) if err != nil { utils.BadRequest(w, "since", query.Since, err) @@ -200,7 +266,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { } var until time.Time - if _, found := mux.Vars(r)["until"]; found { + if _, found := r.URL.Query()["until"]; found { since, err = util.ParseInputTime(query.Until) if err != nil { utils.BadRequest(w, "until", query.Until, err) @@ -233,7 +299,7 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { var builder strings.Builder for ok := true; ok; ok = query.Follow { for line := range logChannel { - if _, found := mux.Vars(r)["until"]; found { + if _, found := r.URL.Query()["until"]; found { if line.Time.After(until) { break } @@ -266,7 +332,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { builder.WriteRune(' ') } builder.WriteString(line.Msg) - // Build header and output entry binary.BigEndian.PutUint32(header[4:], uint32(len(header)+builder.Len())) if _, err := w.Write(header[:]); err != nil { @@ -275,7 +340,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { if _, err := fmt.Fprint(w, builder.String()); err != nil { log.Errorf("unable to write builder string: %q", err) } - if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } diff --git a/pkg/api/handlers/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index facbd22a5..da7b5bb0c 100644 --- a/pkg/api/handlers/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "net/http" @@ -6,7 +6,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -30,12 +29,10 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - muxVars := mux.Vars(r) - // Detach keys: explicitly set to "" is very different from unset // TODO: Our format for parsing these may be different from Docker. var detachKeys *string - if _, found := muxVars["detachKeys"]; found { + if _, found := r.URL.Query()["detachKeys"]; found { detachKeys = &query.DetachKeys } @@ -44,15 +41,15 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { streams.Stderr = true streams.Stdin = true useStreams := false - if _, found := muxVars["stdin"]; found { + if _, found := r.URL.Query()["stdin"]; found { streams.Stdin = query.Stdin useStreams = true } - if _, found := muxVars["stdout"]; found { + if _, found := r.URL.Query()["stdout"]; found { streams.Stdout = query.Stdout useStreams = true } - if _, found := muxVars["stderr"]; found { + if _, found := r.URL.Query()["stderr"]; found { streams.Stderr = query.Stderr useStreams = true } @@ -72,7 +69,7 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } // We only support stream=true or unset - if _, found := muxVars["stream"]; found && query.Stream { + if _, found := r.URL.Query()["stream"]; found && query.Stream { utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the stream parameter to attach is not presently supported")) return } diff --git a/pkg/api/handlers/generic/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 7e542752f..12af40876 100644 --- a/pkg/api/handlers/generic/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/json" @@ -6,8 +6,8 @@ import ( "net/http" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" image2 "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" @@ -46,7 +46,12 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "NewFromLocal()")) return } - cc, err := makeCreateConfig(input, newImage) + defaultContainerConfig, err := runtime.GetConfig() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "GetConfig()")) + return + } + cc, err := makeCreateConfig(defaultContainerConfig, input, newImage) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) return @@ -55,7 +60,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.CreateContainer(r.Context(), w, runtime, &cc) } -func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(defaultContainerConfig *config.Config, input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool @@ -76,7 +81,7 @@ func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Ima workDir = input.WorkingDir } - stopTimeout := uint(define.CtrRemoveTimeout) + stopTimeout := defaultContainerConfig.Engine.StopTimeout if input.StopTimeout != nil { stopTimeout = uint(*input.StopTimeout) } diff --git a/pkg/api/handlers/compat/containers_pause.go b/pkg/api/handlers/compat/containers_pause.go new file mode 100644 index 000000000..060bdbaeb --- /dev/null +++ b/pkg/api/handlers/compat/containers_pause.go @@ -0,0 +1,28 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +func PauseContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /{version}/containers/(name)/pause + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + // the api does not error if the Container is already paused, so just into it + if err := con.Pause(); err != nil { + utils.InternalServerError(w, err) + return + } + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go new file mode 100644 index 000000000..a56c3903d --- /dev/null +++ b/pkg/api/handlers/compat/containers_prune.go @@ -0,0 +1,64 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/docker/docker/api/types" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func PruneContainers(w http.ResponseWriter, r *http.Request) { + var ( + delContainers []string + space int64 + ) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + Filters map[string][]string `schema:"filters"` + }{} + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + filterFuncs, err := utils.GenerateFilterFuncsFromMap(runtime, query.Filters) + if err != nil { + utils.InternalServerError(w, err) + return + } + prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) + if err != nil { + utils.InternalServerError(w, err) + return + } + + // Libpod response differs + if utils.IsLibpodRequest(r) { + var response []handlers.LibpodContainersPruneReport + for ctrID, size := range prunedContainers { + response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, SpaceReclaimed: size}) + } + for ctrID, err := range pruneErrors { + response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, PruneError: err.Error()}) + } + utils.WriteResponse(w, http.StatusOK, response) + return + } + for ctrID, size := range prunedContainers { + if pruneErrors[ctrID] == nil { + space += size + delContainers = append(delContainers, ctrID) + } + } + report := types.ContainersPruneReport{ + ContainersDeleted: delContainers, + SpaceReclaimed: uint64(space), + } + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/compat/containers_restart.go b/pkg/api/handlers/compat/containers_restart.go new file mode 100644 index 000000000..343bf96d2 --- /dev/null +++ b/pkg/api/handlers/compat/containers_restart.go @@ -0,0 +1,45 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func RestartContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + // /{version}/containers/(name)/restart + query := struct { + Timeout int `schema:"t"` + }{ + // Override golang default values for types + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.BadRequest(w, "url", r.URL.String(), errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + timeout := con.StopTimeout() + if _, found := r.URL.Query()["t"]; found { + timeout = uint(query.Timeout) + } + + if err := con.RestartWithTimeout(r.Context(), timeout); err != nil { + utils.InternalServerError(w, err) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/compat/containers_start.go b/pkg/api/handlers/compat/containers_start.go new file mode 100644 index 000000000..67bd287ab --- /dev/null +++ b/pkg/api/handlers/compat/containers_start.go @@ -0,0 +1,51 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func StartContainer(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + DetachKeys string `schema:"detachKeys"` + }{ + // Override golang default values for types + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.BadRequest(w, "url", r.URL.String(), err) + return + } + if len(query.DetachKeys) > 0 { + // TODO - start does not support adding detach keys + utils.BadRequest(w, "detachKeys", query.DetachKeys, errors.New("the detachKeys parameter is not supported yet")) + return + } + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := con.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + if state == define.ContainerStateRunning { + utils.WriteResponse(w, http.StatusNotModified, "") + return + } + if err := con.Start(r.Context(), false); err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/compat/containers_stats.go index 977979741..53ad0a632 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/compat/containers_stats.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/json" diff --git a/pkg/api/handlers/containers_top.go b/pkg/api/handlers/compat/containers_top.go index 06d5dd653..202be55d1 100644 --- a/pkg/api/handlers/containers_top.go +++ b/pkg/api/handlers/compat/containers_top.go @@ -1,10 +1,11 @@ -package handlers +package compat import ( "net/http" "strings" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -42,7 +43,7 @@ func TopContainer(w http.ResponseWriter, r *http.Request) { return } - var body = ContainerTopOKBody{} + var body = handlers.ContainerTopOKBody{} if len(output) > 0 { body.Titles = strings.Split(output[0], "\t") for _, line := range output[1:] { diff --git a/pkg/api/handlers/compat/containers_unpause.go b/pkg/api/handlers/compat/containers_unpause.go new file mode 100644 index 000000000..adabdeaea --- /dev/null +++ b/pkg/api/handlers/compat/containers_unpause.go @@ -0,0 +1,28 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +func UnpauseContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /{version}/containers/(name)/unpause + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + if err := con.Unpause(); err != nil { + utils.InternalServerError(w, err) + return + } + + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/compat/events.go index 22dad9923..0f72ef328 100644 --- a/pkg/api/handlers/events.go +++ b/pkg/api/handlers/compat/events.go @@ -1,12 +1,15 @@ -package handlers +package compat import ( "encoding/json" "fmt" "net/http" + "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/events" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -15,13 +18,16 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { var ( fromStart bool eventsError error + decoder = r.Context().Value("decoder").(*schema.Decoder) + runtime = r.Context().Value("runtime").(*libpod.Runtime) ) + query := struct { Since string `schema:"since"` Until string `schema:"until"` Filters map[string][]string `schema:"filters"` }{} - if err := decodeQuery(r, &query); err != nil { + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) } @@ -38,19 +44,20 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { eventChannel := make(chan *events.Event) go func() { readOpts := events.ReadOptions{FromStart: fromStart, Stream: true, Filters: libpodFilters, EventChannel: eventChannel, Since: query.Since, Until: query.Until} - eventsError = getRuntime(r).Events(readOpts) + eventsError = runtime.Events(readOpts) }() if eventsError != nil { utils.InternalServerError(w, eventsError) return } + + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) for event := range eventChannel { - e := EventToApiEvent(event) - //utils.WriteJSON(w, http.StatusOK, e) - coder := json.NewEncoder(w) - coder.SetEscapeHTML(true) + e := handlers.EventToApiEvent(event) if err := coder.Encode(e); err != nil { logrus.Errorf("unable to write json: %q", err) } diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go new file mode 100644 index 000000000..ec1a8ac96 --- /dev/null +++ b/pkg/api/handlers/compat/exec.go @@ -0,0 +1,107 @@ +package compat + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ExecCreateHandler creates an exec session for a given container. +func ExecCreateHandler(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + input := new(handlers.ExecCreateConfig) + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error decoding request body as JSON")) + return + } + + ctrName := utils.GetName(r) + ctr, err := runtime.LookupContainer(ctrName) + if err != nil { + utils.ContainerNotFound(w, ctrName, err) + return + } + + libpodConfig := new(libpod.ExecConfig) + libpodConfig.Command = input.Cmd + libpodConfig.Terminal = input.Tty + libpodConfig.AttachStdin = input.AttachStdin + libpodConfig.AttachStderr = input.AttachStderr + libpodConfig.AttachStdout = input.AttachStdout + if input.DetachKeys != "" { + libpodConfig.DetachKeys = &input.DetachKeys + } + libpodConfig.Environment = make(map[string]string) + for _, envStr := range input.Env { + split := strings.SplitN(envStr, "=", 2) + if len(split) != 2 { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Errorf("environment variable %q badly formed, must be key=value", envStr)) + return + } + libpodConfig.Environment[split[0]] = split[1] + } + libpodConfig.WorkDir = input.WorkingDir + libpodConfig.Privileged = input.Privileged + libpodConfig.User = input.User + + sessID, err := ctr.ExecCreate(libpodConfig) + if err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid { + // Check if the container is paused. If so, return a 409 + state, err := ctr.State() + if err == nil { + // Ignore the error != nil case. We're already + // throwing an InternalServerError below. + if state == define.ContainerStatePaused { + utils.Error(w, "Container is paused", http.StatusConflict, errors.Errorf("cannot create exec session as container %s is paused", ctr.ID())) + return + } + } + } + utils.InternalServerError(w, err) + return + } + + resp := new(handlers.ExecCreateResponse) + resp.ID = sessID + + utils.WriteResponse(w, http.StatusCreated, resp) +} + +// ExecInspectHandler inspects a given exec session. +func ExecInspectHandler(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + sessionID := mux.Vars(r)["id"] + sessionCtr, err := runtime.GetExecSessionContainer(sessionID) + if err != nil { + utils.Error(w, fmt.Sprintf("No such exec session: %s", sessionID), http.StatusNotFound, err) + return + } + + logrus.Debugf("Inspecting exec session %s of container %s", sessionID, sessionCtr.ID()) + + session, err := sessionCtr.ExecSession(sessionID) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error retrieving exec session %s from container %s", sessionID, sessionCtr.ID())) + return + } + + inspectOut, err := session.Inspect() + if err != nil { + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, inspectOut) +} diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/compat/images.go index 1ced499d9..ea9cbd691 100644 --- a/pkg/api/handlers/generic/images.go +++ b/pkg/api/handlers/compat/images.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "encoding/json" @@ -15,6 +15,7 @@ import ( image2 "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" "github.com/docker/docker/api/types" "github.com/gorilla/schema" @@ -63,6 +64,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { + All bool Filters map[string][]string `schema:"filters"` }{ // This is where you can override the golang default value for one of fields @@ -79,7 +81,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { filters = append(filters, fmt.Sprintf("%s=%s", k, val)) } } - pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), false, filters) + pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, filters) if err != nil { utils.InternalServerError(w, err) return @@ -90,7 +92,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { }) } - //FIXME/TODO to do this exacty correct, pruneimages needs to return idrs and space-reclaimed, then we are golden + //FIXME/TODO to do this exactly correct, pruneimages needs to return idrs and space-reclaimed, then we are golden ipr := types.ImagesPruneReport{ ImagesDeleted: idr, SpaceReclaimed: 1, // TODO we cannot supply this right now @@ -127,13 +129,13 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } - sc := image2.GetSystemContext(rtc.SignaturePolicyPath, "", false) + sc := image2.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) tag := "latest" options := libpod.ContainerCommitOptions{ Pause: true, } options.CommitOptions = buildah.CommitOptions{ - SignaturePolicyPath: rtc.SignaturePolicyPath, + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, ReportWriter: os.Stderr, SystemContext: sc, PreferredManifestType: manifest.DockerV2Schema2MediaType, @@ -198,7 +200,7 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { return } source = f.Name() - if err := handlers.SaveFromBody(f, r); err != nil { + if err := SaveFromBody(f, r); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) } } @@ -286,7 +288,7 @@ func GetImage(w http.ResponseWriter, r *http.Request) { // 404 no such // 500 internal name := utils.GetName(r) - newImage, err := handlers.GetImage(r, name) + newImage, err := utils.GetImage(r, name) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) return @@ -305,7 +307,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) return } - var summaries = make([]*handlers.ImageSummary, len(images)+1) + var summaries = make([]*entities.ImageSummary, len(images)) for j, img := range images { is, err := handlers.ImageToImageSummary(img) if err != nil { @@ -344,7 +346,7 @@ func LoadImages(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) return } - if err := handlers.SaveFromBody(f, r); err != nil { + if err := SaveFromBody(f, r); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) return } diff --git a/pkg/api/handlers/images_build.go b/pkg/api/handlers/compat/images_build.go index b29c45574..e208e6ddc 100644 --- a/pkg/api/handlers/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "bytes" @@ -15,13 +15,15 @@ import ( "github.com/containers/buildah" "github.com/containers/buildah/imagebuildah" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/storage/pkg/archive" - "github.com/gorilla/mux" + "github.com/gorilla/schema" ) func BuildImage(w http.ResponseWriter, r *http.Request) { - authConfigs := map[string]AuthConfig{} + authConfigs := map[string]handlers.AuthConfig{} if hdr, found := r.Header["X-Registry-Config"]; found && len(hdr) > 0 { authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(hdr[0])) if json.NewDecoder(authConfigsJSON).Decode(&authConfigs) != nil { @@ -97,8 +99,8 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Outputs: "", Registry: "docker.io", } - - if err := decodeQuery(r, &query); err != nil { + decoder := r.Context().Value("decoder").(*schema.Decoder) + if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return } @@ -114,24 +116,24 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { tag = tokens[1] } - if t, found := mux.Vars(r)["target"]; found { - name = t + if _, found := r.URL.Query()["target"]; found { + name = query.Target } var buildArgs = map[string]string{} - if a, found := mux.Vars(r)["buildargs"]; found { - if err := json.Unmarshal([]byte(a), &buildArgs); err != nil { - utils.BadRequest(w, "buildargs", a, err) + if _, found := r.URL.Query()["buildargs"]; found { + if err := json.Unmarshal([]byte(query.BuildArgs), &buildArgs); err != nil { + utils.BadRequest(w, "buildargs", query.BuildArgs, err) return } } // convert label formats var labels = []string{} - if l, found := mux.Vars(r)["labels"]; found { + if _, found := r.URL.Query()["labels"]; found { var m = map[string]string{} - if err := json.Unmarshal([]byte(l), &m); err != nil { - utils.BadRequest(w, "labels", l, err) + if err := json.Unmarshal([]byte(query.Labels), &m); err != nil { + utils.BadRequest(w, "labels", query.Labels, err) return } @@ -141,7 +143,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } pullPolicy := buildah.PullIfMissing - if _, found := mux.Vars(r)["pull"]; found { + if _, found := r.URL.Query()["pull"]; found { if query.Pull { pullPolicy = buildah.PullAlways } @@ -220,7 +222,8 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Devices: nil, } - id, _, err := getRuntime(r).Build(r.Context(), buildOptions, query.Dockerfile) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + id, _, err := runtime.Build(r.Context(), buildOptions, query.Dockerfile) if err != nil { utils.InternalServerError(w, err) } diff --git a/pkg/api/handlers/compat/images_history.go b/pkg/api/handlers/compat/images_history.go new file mode 100644 index 000000000..afadf4c48 --- /dev/null +++ b/pkg/api/handlers/compat/images_history.go @@ -0,0 +1,40 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func HistoryImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + var allHistory []handlers.HistoryResponse + + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + + } + history, err := newImage.History(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return + } + for _, h := range history { + l := handlers.HistoryResponse{ + ID: h.ID, + Created: h.Created.Unix(), + CreatedBy: h.CreatedBy, + Tags: h.Tags, + Size: h.Size, + Comment: h.Comment, + } + allHistory = append(allHistory, l) + } + utils.WriteResponse(w, http.StatusOK, allHistory) +} diff --git a/pkg/api/handlers/compat/images_remove.go b/pkg/api/handlers/compat/images_remove.go new file mode 100644 index 000000000..ed0153529 --- /dev/null +++ b/pkg/api/handlers/compat/images_remove.go @@ -0,0 +1,58 @@ +package compat + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func RemoveImage(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Force bool `schema:"force"` + NoPrune bool `schema:"noprune"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + if _, found := r.URL.Query()["noprune"]; found { + if query.NoPrune { + utils.UnSupportedParameter("noprune") + } + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + + results, err := runtime.RemoveImage(r.Context(), newImage, query.Force) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + response := make([]map[string]string, 0, len(results.Untagged)+1) + deleted := make(map[string]string, 1) + deleted["Deleted"] = results.Deleted + response = append(response, deleted) + + for _, u := range results.Untagged { + untagged := make(map[string]string, 1) + untagged["Untagged"] = u + response = append(response, untagged) + } + + utils.WriteResponse(w, http.StatusOK, response) + +} diff --git a/pkg/api/handlers/compat/images_save.go b/pkg/api/handlers/compat/images_save.go new file mode 100644 index 000000000..b39c719a0 --- /dev/null +++ b/pkg/api/handlers/compat/images_save.go @@ -0,0 +1,14 @@ +package compat + +import ( + "io" + "net/http" + "os" +) + +func SaveFromBody(f *os.File, r *http.Request) error { // nolint + if _, err := io.Copy(f, r.Body); err != nil { + return err + } + return f.Close() +} diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go new file mode 100644 index 000000000..7283b22c4 --- /dev/null +++ b/pkg/api/handlers/compat/images_search.go @@ -0,0 +1,66 @@ +package compat + +import ( + "net/http" + "strconv" + + "github.com/containers/image/v5/types" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func SearchImages(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Term string `json:"term"` + Limit int `json:"limit"` + Filters map[string][]string `json:"filters"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + filter := image.SearchFilter{} + if len(query.Filters) > 0 { + if len(query.Filters["stars"]) > 0 { + stars, err := strconv.Atoi(query.Filters["stars"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.Stars = stars + } + if len(query.Filters["is-official"]) > 0 { + isOfficial, err := strconv.ParseBool(query.Filters["is-official"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.IsOfficial = types.NewOptionalBool(isOfficial) + } + if len(query.Filters["is-automated"]) > 0 { + isAutomated, err := strconv.ParseBool(query.Filters["is-automated"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + filter.IsAutomated = types.NewOptionalBool(isAutomated) + } + } + options := image.SearchOptions{ + Filter: filter, + Limit: query.Limit, + } + results, err := image.SearchImages(query.Term, options) + if err != nil { + utils.BadRequest(w, "term", query.Term, err) + return + } + utils.WriteResponse(w, http.StatusOK, results) +} diff --git a/pkg/api/handlers/compat/images_tag.go b/pkg/api/handlers/compat/images_tag.go new file mode 100644 index 000000000..722be5653 --- /dev/null +++ b/pkg/api/handlers/compat/images_tag.go @@ -0,0 +1,37 @@ +package compat + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func TagImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + // /v1.xx/images/(name)/tag + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + tag := "latest" + if len(r.Form.Get("tag")) > 0 { + tag = r.Form.Get("tag") + } + if len(r.Form.Get("repo")) < 1 { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) + return + } + repo := r.Form.Get("repo") + tagName := fmt.Sprintf("%s:%s", repo, tag) + if err := newImage.TagImage(tagName); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusCreated, "") +} diff --git a/pkg/api/handlers/generic/info.go b/pkg/api/handlers/compat/info.go index c9e79233d..104d0793b 100644 --- a/pkg/api/handlers/generic/info.go +++ b/pkg/api/handlers/compat/info.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "fmt" @@ -9,8 +9,8 @@ import ( "strings" "time" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/config" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" @@ -60,7 +60,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { CPUCfsQuota: sysInfo.CPUCfsQuota, CPUSet: sysInfo.Cpuset, CPUShares: sysInfo.CPUShares, - CgroupDriver: configInfo.CgroupManager, + CgroupDriver: configInfo.Engine.CgroupManager, ClusterAdvertise: "", ClusterStore: "", ContainerdCommit: docker.Commit{}, @@ -69,7 +69,7 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { ContainersRunning: stateInfo[define.ContainerStateRunning], ContainersStopped: stateInfo[define.ContainerStateStopped] + stateInfo[define.ContainerStateExited], Debug: log.IsLevelEnabled(log.DebugLevel), - DefaultRuntime: configInfo.OCIRuntime, + DefaultRuntime: configInfo.Engine.OCIRuntime, DockerRootDir: storeInfo["GraphRoot"].(string), Driver: storeInfo["GraphDriverName"].(string), DriverStatus: getGraphStatus(storeInfo), @@ -152,7 +152,7 @@ func getSecOpts(sysInfo *sysinfo.SysInfo) []string { func getRuntimes(configInfo *config.Config) map[string]docker.Runtime { var runtimes = map[string]docker.Runtime{} - for name, paths := range configInfo.OCIRuntimes { + for name, paths := range configInfo.Engine.OCIRuntimes { runtimes[name] = docker.Runtime{ Path: paths[0], Args: nil, diff --git a/pkg/api/handlers/ping.go b/pkg/api/handlers/compat/ping.go index d41da60f3..6e77e270f 100644 --- a/pkg/api/handlers/ping.go +++ b/pkg/api/handlers/compat/ping.go @@ -1,10 +1,11 @@ -package handlers +package compat import ( "fmt" "net/http" "github.com/containers/buildah" + "github.com/containers/libpod/pkg/api/handlers" ) // Ping returns headers to client about the service @@ -12,14 +13,14 @@ import ( // This handler must always be the same for the compatibility and libpod URL trees! // Clients will use the Header availability to test which backend engine is in use. func Ping(w http.ResponseWriter, r *http.Request) { - w.Header().Set("API-Version", DefaultApiVersion) + w.Header().Set("API-Version", handlers.DefaultApiVersion) w.Header().Set("BuildKit-Version", "") w.Header().Set("Docker-Experimental", "true") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Pragma", "no-cache") // API-Version and Libpod-API-Version may not always be equal - w.Header().Set("Libpod-API-Version", DefaultApiVersion) + w.Header().Set("Libpod-API-Version", handlers.DefaultApiVersion) w.Header().Set("Libpod-Buildha-Version", buildah.Version) w.WriteHeader(http.StatusOK) diff --git a/pkg/api/handlers/generic/swagger.go b/pkg/api/handlers/compat/swagger.go index c9c9610bb..cbd8e61fb 100644 --- a/pkg/api/handlers/generic/swagger.go +++ b/pkg/api/handlers/compat/swagger.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "github.com/containers/libpod/pkg/api/handlers/utils" diff --git a/pkg/api/handlers/generic/system.go b/pkg/api/handlers/compat/system.go index edf1f8522..47e187ba1 100644 --- a/pkg/api/handlers/generic/system.go +++ b/pkg/api/handlers/compat/system.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "net/http" diff --git a/pkg/api/handlers/generic/types.go b/pkg/api/handlers/compat/types.go index f068ac011..b8d06760f 100644 --- a/pkg/api/handlers/generic/types.go +++ b/pkg/api/handlers/compat/types.go @@ -1,4 +1,4 @@ -package generic +package compat import ( "time" diff --git a/pkg/api/handlers/unsupported.go b/pkg/api/handlers/compat/unsupported.go index 956d31f8b..d9c3c3f49 100644 --- a/pkg/api/handlers/unsupported.go +++ b/pkg/api/handlers/compat/unsupported.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "fmt" diff --git a/pkg/api/handlers/version.go b/pkg/api/handlers/compat/version.go index 94166952c..c7f7917ac 100644 --- a/pkg/api/handlers/version.go +++ b/pkg/api/handlers/compat/version.go @@ -1,4 +1,4 @@ -package handlers +package compat import ( "fmt" @@ -8,16 +8,12 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" docker "github.com/docker/docker/api/types" "github.com/pkg/errors" ) -const ( - DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/ - MinimalApiVersion = "1.24" -) - func VersionHandler(w http.ResponseWriter, r *http.Request) { // 200 ok // 500 internal @@ -40,19 +36,19 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) { Name: "Podman Engine", Version: versionInfo.Version, Details: map[string]string{ - "APIVersion": DefaultApiVersion, + "APIVersion": handlers.DefaultApiVersion, "Arch": goRuntime.GOARCH, "BuildTime": time.Unix(versionInfo.Built, 0).Format(time.RFC3339), "Experimental": "true", "GitCommit": versionInfo.GitCommit, "GoVersion": versionInfo.GoVersion, "KernelVersion": hostInfo["kernel"].(string), - "MinAPIVersion": MinimalApiVersion, + "MinAPIVersion": handlers.MinimalApiVersion, "Os": goRuntime.GOOS, }, }} - utils.WriteResponse(w, http.StatusOK, Version{Version: docker.Version{ + utils.WriteResponse(w, http.StatusOK, handlers.Version{Version: docker.Version{ Platform: struct { Name string }{ diff --git a/pkg/api/handlers/containers.go b/pkg/api/handlers/containers.go deleted file mode 100644 index d7d040ce2..000000000 --- a/pkg/api/handlers/containers.go +++ /dev/null @@ -1,245 +0,0 @@ -package handlers - -import ( - "fmt" - "github.com/docker/docker/api/types" - "net/http" - - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" - "github.com/gorilla/schema" - "github.com/pkg/errors" -) - -func StopContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - - // /{version}/containers/(name)/stop - query := struct { - Timeout int `schema:"t"` - }{ - // override any golang type defaults - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - name := utils.GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - utils.InternalServerError(w, errors.Wrapf(err, "unable to get state for Container %s", name)) - return - } - // If the Container is stopped already, send a 304 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - utils.WriteResponse(w, http.StatusNotModified, "") - return - } - - var stopError error - if query.Timeout > 0 { - stopError = con.StopWithTimeout(uint(query.Timeout)) - } else { - stopError = con.Stop() - } - if stopError != nil { - utils.InternalServerError(w, errors.Wrapf(stopError, "failed to stop %s", name)) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func UnpauseContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /{version}/containers/(name)/unpause - name := utils.GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - // the api does not error if the Container is already paused, so just into it - if err := con.Unpause(); err != nil { - utils.InternalServerError(w, err) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func PauseContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /{version}/containers/(name)/pause - name := utils.GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - // the api does not error if the Container is already paused, so just into it - if err := con.Pause(); err != nil { - utils.InternalServerError(w, err) - return - } - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func StartContainer(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - DetachKeys string `schema:"detachKeys"` - }{ - // Override golang default values for types - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - if len(query.DetachKeys) > 0 { - // TODO - start does not support adding detach keys - utils.Error(w, "Something went wrong", http.StatusBadRequest, errors.New("the detachKeys parameter is not supported yet")) - return - } - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := utils.GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - utils.InternalServerError(w, err) - return - } - if state == define.ContainerStateRunning { - utils.WriteResponse(w, http.StatusNotModified, "") - return - } - if err := con.Start(r.Context(), false); err != nil { - utils.InternalServerError(w, err) - return - } - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func RestartContainer(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - // /{version}/containers/(name)/restart - query := struct { - Timeout int `schema:"t"` - }{ - // Override golang default values for types - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - name := utils.GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - utils.ContainerNotFound(w, name, err) - return - } - - state, err := con.State() - if err != nil { - utils.InternalServerError(w, err) - return - } - - // FIXME: This is not in the swagger.yml... - // If the Container is stopped already, send a 409 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - msg := fmt.Sprintf("Container %s is not running", name) - utils.Error(w, msg, http.StatusConflict, errors.New(msg)) - return - } - - timeout := con.StopTimeout() - if _, found := mux.Vars(r)["t"]; found { - timeout = uint(query.Timeout) - } - - if err := con.RestartWithTimeout(r.Context(), timeout); err != nil { - utils.InternalServerError(w, err) - return - } - - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - -func PruneContainers(w http.ResponseWriter, r *http.Request) { - var ( - delContainers []string - space int64 - ) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - - query := struct { - Filters map[string][]string `schema:"filter"` - }{} - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - filterFuncs, err := utils.GenerateFilterFuncsFromMap(runtime, query.Filters) - if err != nil { - utils.InternalServerError(w, err) - return - } - prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) - if err != nil { - utils.InternalServerError(w, err) - return - } - - // Libpod response differs - if utils.IsLibpodRequest(r) { - var response []LibpodContainersPruneReport - for ctrID, size := range prunedContainers { - response = append(response, LibpodContainersPruneReport{ID: ctrID, SpaceReclaimed: size}) - } - for ctrID, err := range pruneErrors { - response = append(response, LibpodContainersPruneReport{ID: ctrID, PruneError: err.Error()}) - } - utils.WriteResponse(w, http.StatusOK, response) - return - } - for ctrID, size := range prunedContainers { - if pruneErrors[ctrID] == nil { - space += size - delContainers = append(delContainers, ctrID) - } - } - report := types.ContainersPruneReport{ - ContainersDeleted: delContainers, - SpaceReclaimed: uint64(space), - } - utils.WriteResponse(w, http.StatusOK, report) -} diff --git a/pkg/api/handlers/exec.go b/pkg/api/handlers/exec.go deleted file mode 100644 index 8a7b2ae26..000000000 --- a/pkg/api/handlers/exec.go +++ /dev/null @@ -1,25 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers/utils" -) - -func CreateExec(w http.ResponseWriter, r *http.Request) { - utils.Error(w, "function not implemented", http.StatusInternalServerError, define.ErrNotImplemented) -} - -func StartExec(w http.ResponseWriter, r *http.Request) { - utils.Error(w, "function not implemented", http.StatusInternalServerError, define.ErrNotImplemented) -} - -func ResizeExec(w http.ResponseWriter, r *http.Request) { - utils.Error(w, "function not implemented", http.StatusInternalServerError, define.ErrNotImplemented) - -} - -func InspectExec(w http.ResponseWriter, r *http.Request) { - utils.Error(w, "function not implemented", http.StatusInternalServerError, define.ErrNotImplemented) -} diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go index 231c11f23..2dd2c886b 100644 --- a/pkg/api/handlers/handler.go +++ b/pkg/api/handlers/handler.go @@ -1,38 +1,6 @@ package handlers -import ( - "net/http" - - "github.com/containers/libpod/libpod" - "github.com/gorilla/schema" - "github.com/pkg/errors" +const ( + DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/ + MinimalApiVersion = "1.24" ) - -// Convenience routines to reduce boiler plate in handlers - -// func hasVar(r *http.Request, k string) bool { -// _, found := mux.Vars(r)[k] -// return found -// } - -func decodeQuery(r *http.Request, i interface{}) error { - decoder := r.Context().Value("decoder").(*schema.Decoder) - - if err := decoder.Decode(i, r.URL.Query()); err != nil { - return errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()) - } - return nil -} - -func getRuntime(r *http.Request) *libpod.Runtime { - return r.Context().Value("runtime").(*libpod.Runtime) -} - -// func getHeader(r *http.Request, k string) string { -// return r.Header.Get(k) -// } -// -// func hasHeader(r *http.Request, k string) bool { -// _, found := r.Header[k] -// return found -// } diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go deleted file mode 100644 index 2086ce748..000000000 --- a/pkg/api/handlers/images.go +++ /dev/null @@ -1,189 +0,0 @@ -package handlers - -import ( - "fmt" - "io" - "net/http" - "os" - "strconv" - - "github.com/containers/image/v5/types" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/image" - "github.com/containers/libpod/pkg/api/handlers/utils" - "github.com/gorilla/mux" - "github.com/gorilla/schema" - "github.com/pkg/errors" -) - -func HistoryImage(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := utils.GetName(r) - var allHistory []HistoryResponse - - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) - return - - } - history, err := newImage.History(r.Context()) - if err != nil { - utils.InternalServerError(w, err) - return - } - for _, h := range history { - l := HistoryResponse{ - ID: h.ID, - Created: h.Created.UnixNano(), - CreatedBy: h.CreatedBy, - Tags: h.Tags, - Size: h.Size, - Comment: h.Comment, - } - allHistory = append(allHistory, l) - } - utils.WriteResponse(w, http.StatusOK, allHistory) -} - -func TagImage(w http.ResponseWriter, r *http.Request) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - // /v1.xx/images/(name)/tag - name := utils.GetName(r) - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) - return - } - tag := "latest" - if len(r.Form.Get("tag")) > 0 { - tag = r.Form.Get("tag") - } - if len(r.Form.Get("repo")) < 1 { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) - return - } - repo := r.Form.Get("repo") - tagName := fmt.Sprintf("%s:%s", repo, tag) - if err := newImage.TagImage(tagName); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - utils.WriteResponse(w, http.StatusCreated, "") -} - -func RemoveImage(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - query := struct { - noPrune bool - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - muxVars := mux.Vars(r) - if _, found := muxVars["noprune"]; found { - if query.noPrune { - utils.UnSupportedParameter("noprune") - } - } - name := utils.GetName(r) - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) - return - } - - force := false - if len(r.Form.Get("force")) > 0 { - force, err = strconv.ParseBool(r.Form.Get("force")) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, err) - return - } - } - _, err = runtime.RemoveImage(r.Context(), newImage, force) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - // TODO - // This will need to be fixed for proper response, like Deleted: and Untagged: - m := make(map[string]string) - m["Deleted"] = newImage.ID() - foo := []map[string]string{} - foo = append(foo, m) - utils.WriteResponse(w, http.StatusOK, foo) - -} -func GetImage(r *http.Request, name string) (*image.Image, error) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - return runtime.ImageRuntime().NewFromLocal(name) -} - -func SaveFromBody(f *os.File, r *http.Request) error { // nolint - if _, err := io.Copy(f, r.Body); err != nil { - return err - } - return f.Close() -} - -func SearchImages(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - Term string `json:"term"` - Limit int `json:"limit"` - Filters map[string][]string `json:"filters"` - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - filter := image.SearchFilter{} - if len(query.Filters) > 0 { - if len(query.Filters["stars"]) > 0 { - stars, err := strconv.Atoi(query.Filters["stars"][0]) - if err != nil { - utils.InternalServerError(w, err) - return - } - filter.Stars = stars - } - if len(query.Filters["is-official"]) > 0 { - isOfficial, err := strconv.ParseBool(query.Filters["is-official"][0]) - if err != nil { - utils.InternalServerError(w, err) - return - } - filter.IsOfficial = types.NewOptionalBool(isOfficial) - } - if len(query.Filters["is-automated"]) > 0 { - isAutomated, err := strconv.ParseBool(query.Filters["is-automated"][0]) - if err != nil { - utils.InternalServerError(w, err) - return - } - filter.IsAutomated = types.NewOptionalBool(isAutomated) - } - } - options := image.SearchOptions{ - Filter: filter, - Limit: query.Limit, - } - results, err := image.SearchImages(query.Term, options) - if err != nil { - utils.BadRequest(w, "term", query.Term, err) - return - } - utils.WriteResponse(w, http.StatusOK, results) -} diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 752b004d8..cdc34004f 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -10,44 +10,27 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -func StopContainer(w http.ResponseWriter, r *http.Request) { - handlers.StopContainer(w, r) -} - func ContainerExists(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) name := utils.GetName(r) _, err := runtime.LookupContainer(name) if err != nil { - utils.ContainerNotFound(w, name, err) + if errors.Cause(err) == define.ErrNoSuchCtr { + utils.ContainerNotFound(w, name, err) + } + utils.InternalServerError(w, err) return + } utils.WriteResponse(w, http.StatusNoContent, "") } -func RemoveContainer(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - Force bool `schema:"force"` - Vols bool `schema:"v"` - }{ - // override any golang type defaults - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - utils.RemoveContainer(w, r, query.Force, query.Vols) -} func ListContainers(w http.ResponseWriter, r *http.Request) { var ( filterFuncs []libpod.ContainerFilter @@ -165,20 +148,9 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, data) } -func KillContainer(w http.ResponseWriter, r *http.Request) { - // /{version}/containers/(name)/kill - _, err := utils.KillContainer(w, r) - if err != nil { - return - } - // Success - utils.WriteResponse(w, http.StatusNoContent, "") -} - func WaitContainer(w http.ResponseWriter, r *http.Request) { exitCode, err := utils.WaitContainer(w, r) if err != nil { - utils.InternalServerError(w, err) return } utils.WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode))) diff --git a/pkg/api/handlers/libpod/healthcheck.go b/pkg/api/handlers/libpod/healthcheck.go index 6c74500b9..6eb2ab0e3 100644 --- a/pkg/api/handlers/libpod/healthcheck.go +++ b/pkg/api/handlers/libpod/healthcheck.go @@ -14,8 +14,30 @@ func RunHealthCheck(w http.ResponseWriter, r *http.Request) { if err != nil { if status == libpod.HealthCheckContainerNotFound { utils.ContainerNotFound(w, name, err) + return } + if status == libpod.HealthCheckNotDefined { + utils.Error(w, "no healthcheck defined", http.StatusConflict, err) + return + } + if status == libpod.HealthCheckContainerStopped { + utils.Error(w, "container not running", http.StatusConflict, err) + return + } + utils.InternalServerError(w, err) + return + } + ctr, err := runtime.LookupContainer(name) + if err != nil { utils.InternalServerError(w, err) + return } - utils.WriteResponse(w, http.StatusOK, status) + + hcLog, err := ctr.GetHealthCheckLog() + if err != nil { + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, hcLog) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 71603e6cc..4b24d7d9f 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -10,14 +10,18 @@ import ( "strconv" "strings" + "github.com/containers/buildah" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" + image2 "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -79,7 +83,7 @@ func ImageTree(w http.ResponseWriter, r *http.Request) { func GetImage(w http.ResponseWriter, r *http.Request) { name := utils.GetName(r) - newImage, err := handlers.GetImage(r, name) + newImage, err := utils.GetImage(r, name) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) return @@ -98,7 +102,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) return } - var summaries = make([]*handlers.ImageSummary, len(images)) + var summaries = make([]*entities.ImageSummary, len(images)) for j, img := range images { is, err := handlers.ImageToImageSummary(img) if err != nil { @@ -106,7 +110,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) { return } // libpod has additional fields that we need to populate. - is.CreatedTime = img.Created() + is.Created = img.Created().Unix() is.ReadOnly = img.IsReadOnly() summaries[j] = is } @@ -115,12 +119,12 @@ func GetImages(w http.ResponseWriter, r *http.Request) { func PruneImages(w http.ResponseWriter, r *http.Request) { var ( - all bool err error ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { + All bool `schema:"all"` Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults @@ -136,7 +140,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { if _, found := r.URL.Query()["filters"]; found { dangling := query.Filters["all"] if len(dangling) > 0 { - all, err = strconv.ParseBool(query.Filters["all"][0]) + query.All, err = strconv.ParseBool(query.Filters["all"][0]) if err != nil { utils.InternalServerError(w, err) return @@ -148,7 +152,8 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) } } - cids, err := runtime.ImageRuntime().PruneImages(r.Context(), all, libpodFilters) + + cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, libpodFilters) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) return @@ -416,3 +421,84 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, res) } + +func CommitContainer(w http.ResponseWriter, r *http.Request) { + var ( + destImage string + mimeType string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Author string `schema:"author"` + Changes []string `schema:"changes"` + Comment string `schema:"comment"` + Container string `schema:"container"` + Format string `schema:"format"` + Pause bool `schema:"pause"` + Repo string `schema:"repo"` + Tag string `schema:"tag"` + }{ + Format: "oci", + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.Error(w, "failed to get runtime config", http.StatusInternalServerError, errors.Wrap(err, "failed to get runtime config")) + return + } + sc := image2.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + tag := "latest" + options := libpod.ContainerCommitOptions{ + Pause: true, + } + switch query.Format { + case "oci": + mimeType = buildah.OCIv1ImageManifest + if len(query.Comment) > 0 { + utils.InternalServerError(w, errors.New("messages are only compatible with the docker image format (-f docker)")) + return + } + case "docker": + mimeType = manifest.DockerV2Schema2MediaType + default: + utils.InternalServerError(w, errors.Errorf("unrecognized image format %q", query.Format)) + return + } + options.CommitOptions = buildah.CommitOptions{ + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, + ReportWriter: os.Stderr, + SystemContext: sc, + PreferredManifestType: mimeType, + } + + if len(query.Tag) > 0 { + tag = query.Tag + } + options.Message = query.Comment + options.Author = query.Author + options.Pause = query.Pause + options.Changes = query.Changes + ctr, err := runtime.LookupContainer(query.Container) + if err != nil { + utils.Error(w, "failed to lookup container", http.StatusNotFound, err) + return + } + + // I know mitr hates this ... but doing for now + if len(query.Repo) > 1 { + destImage = fmt.Sprintf("%s:%s", query.Repo, tag) + } + + commitImage, err := ctr.Commit(r.Context(), destImage, options) + if err != nil && !strings.Contains(err.Error(), "is not running") { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "CommitFailure")) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: commitImage.ID()}) // nolint +} diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go new file mode 100644 index 000000000..d87ed7eba --- /dev/null +++ b/pkg/api/handlers/libpod/manifests.go @@ -0,0 +1,166 @@ +package libpod + +import ( + "encoding/json" + "net/http" + + "github.com/containers/buildah/manifests" + copy2 "github.com/containers/image/v5/copy" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" +) + +func ManifestCreate(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Name []string `schema:"name"` + Image []string `schema:"image"` + All bool `schema:"all"` + }{ + // Add defaults here once needed. + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + manID, err := image.CreateManifestList(runtime.ImageRuntime(), *sc, query.Name, query.Image, query.All) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: manID}) +} + +func ManifestInspect(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + data, err := newImage.InspectManifest() + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, data) +} + +func ManifestAdd(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + var manifestInput image.ManifestAddOpts + if err := json.NewDecoder(r.Body).Decode(&manifestInput); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + newID, err := newImage.AddManifest(*sc, manifestInput) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID}) +} + +func ManifestRemove(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Digest string `schema:"digest"` + }{ + // Add defaults here once needed. + } + name := utils.GetName(r) + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + d, err := digest.Parse(query.Digest) + if err != nil { + utils.Error(w, "invalid digest", http.StatusBadRequest, err) + return + } + newID, err := newImage.RemoveManifest(d) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: newID}) +} +func ManifestPush(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + All bool `schema:"all"` + Destination string `schema:"destination"` + }{ + // Add defaults here once needed. + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + dest, err := alltransports.ParseImageName(query.Destination) + if err != nil { + utils.Error(w, "invalid destination parameter", http.StatusBadRequest, errors.Errorf("invalid destination parameter %q", query.Destination)) + return + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.InternalServerError(w, err) + return + } + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) + opts := manifests.PushOptions{ + ImageListSelection: copy2.CopySpecificImages, + SystemContext: sc, + } + if query.All { + opts.ImageListSelection = copy2.CopyAllImages + } + newD, err := newImage.PushManifest(dest, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, newD.String()) +} diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index e3dbe3b35..e8a92e93e 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -18,7 +18,7 @@ func ListNetworks(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - configDir := config.CNIConfigDir + configDir := config.Network.NetworkConfigDir if len(configDir) < 1 { configDir = network.CNIConfigDir } diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index f5700579b..7e9c2e2c0 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -4,16 +4,14 @@ import ( "encoding/json" "fmt" "net/http" - "strings" - "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/shared/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -21,76 +19,14 @@ import ( func PodCreate(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) - options []libpod.PodCreateOption err error ) - labels := make(map[string]string) - input := handlers.PodCreateConfig{} - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + var psg specgen.PodSpecGenerator + if err := json.NewDecoder(r.Body).Decode(&psg); err != nil { + utils.Error(w, "Failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - if len(input.InfraCommand) > 0 || len(input.InfraImage) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, - errors.New("infra-command and infra-image are not implemented yet")) - return - } - // TODO long term we should break the following out of adapter and into libpod proper - // so that the cli and api can share the creation of a pod with the same options - if len(input.CGroupParent) > 0 { - options = append(options, libpod.WithPodCgroupParent(input.CGroupParent)) - } - - if len(input.Labels) > 0 { - labels, err = parse.GetAllLabels([]string{}, input.Labels) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - } - - if len(labels) != 0 { - options = append(options, libpod.WithPodLabels(labels)) - } - - if len(input.Name) > 0 { - options = append(options, libpod.WithPodName(input.Name)) - } - - if len(input.Hostname) > 0 { - options = append(options, libpod.WithPodHostname(input.Hostname)) - } - - if input.Infra { - // TODO infra-image and infra-command are not supported in the libpod API yet. Will fix - // when implemented in libpod - options = append(options, libpod.WithInfraContainer()) - sharedNamespaces := shared.DefaultKernelNamespaces - if len(input.Share) > 0 { - sharedNamespaces = input.Share - } - nsOptions, err := shared.GetNamespaceOptions(strings.Split(sharedNamespaces, ",")) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - options = append(options, nsOptions...) - } - - if len(input.Publish) > 0 { - portBindings, err := shared.CreatePortBindings(input.Publish) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) - return - } - options = append(options, libpod.WithInfraContainerPorts(portBindings)) - - } - // always have containers use pod cgroups - // User Opt out is not yet supported - options = append(options, libpod.WithPodCgroups()) - - pod, err := runtime.NewPod(r.Context(), options...) + pod, err := psg.MakePod(runtime) if err != nil { http_code := http.StatusInternalServerError if errors.Cause(err) == define.ErrPodExists { @@ -103,10 +39,6 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { } func Pods(w http.ResponseWriter, r *http.Request) { - var ( - runtime = r.Context().Value("runtime").(*libpod.Runtime) - podInspectData []*libpod.PodInspect - ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { Filters map[string][]string `schema:"filters"` @@ -119,25 +51,12 @@ func Pods(w http.ResponseWriter, r *http.Request) { return } - if len(query.Filters) > 0 { - utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) - return - } - - pods, err := runtime.GetAllPods() + pods, err := utils.GetPods(w, r) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - for _, pod := range pods { - data, err := pod.Inspect() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - podInspectData = append(podInspectData, data) - } - utils.WriteResponse(w, http.StatusOK, podInspectData) + utils.WriteResponse(w, http.StatusOK, pods) } func PodInspect(w http.ResponseWriter, r *http.Request) { @@ -161,6 +80,8 @@ func PodStop(w http.ResponseWriter, r *http.Request) { stopError error runtime = r.Context().Value("runtime").(*libpod.Runtime) decoder = r.Context().Value("decoder").(*schema.Decoder) + responses map[string]error + errs []error ) query := struct { Timeout int `schema:"t"` @@ -173,7 +94,6 @@ func PodStop(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - allContainersStopped := true name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { @@ -181,80 +101,68 @@ func PodStop(w http.ResponseWriter, r *http.Request) { return } - // TODO we need to implement a pod.State/Status in libpod internal so libpod api - // users dont have to run through all containers. - podContainers, err := pod.AllContainers() + status, err := pod.GetPodStatus() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - - for _, con := range podContainers { - containerState, err := con.State() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - if containerState == define.ContainerStateRunning { - allContainersStopped = false - break - } - } - if allContainersStopped { + if status != define.PodStateRunning { utils.WriteResponse(w, http.StatusNotModified, "") return } if query.Timeout > 0 { - _, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) + responses, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) } else { - _, stopError = pod.Stop(r.Context(), false) + responses, stopError = pod.Stop(r.Context(), false) } if stopError != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodStopReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodStart(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) - allContainersRunning := true name := utils.GetName(r) pod, err := runtime.LookupPod(name) if err != nil { utils.PodNotFound(w, name, err) return } - - // TODO we need to implement a pod.State/Status in libpod internal so libpod api - // users dont have to run through all containers. - podContainers, err := pod.AllContainers() + status, err := pod.GetPodStatus() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - - for _, con := range podContainers { - containerState, err := con.State() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - if containerState != define.ContainerStateRunning { - allContainersRunning = false - break - } - } - if allContainersRunning { + if status == define.PodStateRunning { utils.WriteResponse(w, http.StatusNotModified, "") return } - if _, err := pod.Start(r.Context()); err != nil { + responses, err := pod.Start(r.Context()) + if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodStartReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodDelete(w http.ResponseWriter, r *http.Request) { @@ -283,10 +191,16 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + report := entities.PodRmReport{ + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodRestart(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) name := utils.GetName(r) pod, err := runtime.LookupPod(name) @@ -294,12 +208,19 @@ func PodRestart(w http.ResponseWriter, r *http.Request) { utils.PodNotFound(w, name, err) return } - _, err = pod.Restart(r.Context()) + responses, err := pod.Restart(r.Context()) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, err := range responses { + errs = append(errs, err) + } + report := entities.PodRestartReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodPrune(w http.ResponseWriter, r *http.Request) { @@ -315,6 +236,9 @@ func PodPrune(w http.ResponseWriter, r *http.Request) { } func PodPause(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) name := utils.GetName(r) pod, err := runtime.LookupPod(name) @@ -322,15 +246,25 @@ func PodPause(w http.ResponseWriter, r *http.Request) { utils.PodNotFound(w, name, err) return } - _, err = pod.Pause() + responses, err := pod.Pause() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + for _, v := range responses { + errs = append(errs, v) + } + report := entities.PodPauseReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodUnpause(w http.ResponseWriter, r *http.Request) { + var ( + errs []error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) name := utils.GetName(r) pod, err := runtime.LookupPod(name) @@ -338,12 +272,19 @@ func PodUnpause(w http.ResponseWriter, r *http.Request) { utils.PodNotFound(w, name, err) return } - _, err = pod.Unpause() + responses, err := pod.Unpause() if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + utils.Error(w, "failed to pause pod", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + for _, v := range responses { + errs = append(errs, v) + } + report := entities.PodUnpauseReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, &report) } func PodKill(w http.ResponseWriter, r *http.Request) { @@ -351,9 +292,10 @@ func PodKill(w http.ResponseWriter, r *http.Request) { runtime = r.Context().Value("runtime").(*libpod.Runtime) decoder = r.Context().Value("decoder").(*schema.Decoder) signal = "SIGKILL" + errs []error ) query := struct { - signal string `schema:"signal"` + Signal string `schema:"signal"` }{ // override any golang type defaults } @@ -362,9 +304,8 @@ func PodKill(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - muxVars := mux.Vars(r) - if _, found := muxVars["signal"]; found { - signal = query.signal + if _, found := r.URL.Query()["signal"]; found { + signal = query.Signal } sig, err := util.ParseSignal(signal) @@ -394,12 +335,23 @@ func PodKill(w http.ResponseWriter, r *http.Request) { utils.Error(w, msg, http.StatusConflict, errors.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) return } - _, err = pod.Kill(uint(sig)) + + responses, err := pod.Kill(uint(sig)) if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + utils.Error(w, "failed to kill pod", http.StatusInternalServerError, err) return } - utils.WriteResponse(w, http.StatusOK, "") + + for _, v := range responses { + if v != nil { + errs = append(errs, v) + } + } + report := &entities.PodKillReport{ + Errs: errs, + Id: pod.ID(), + } + utils.WriteResponse(w, http.StatusOK, report) } func PodExists(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/libpod/swagger.go b/pkg/api/handlers/libpod/swagger.go index aec30ef56..1fad2dd1a 100644 --- a/pkg/api/handlers/libpod/swagger.go +++ b/pkg/api/handlers/libpod/swagger.go @@ -1,8 +1,94 @@ package libpod +import ( + "net/http" + "os" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +// DefaultPodmanSwaggerSpec provides the default path to the podman swagger spec file +const DefaultPodmanSwaggerSpec = "/usr/share/containers/podman/swagger.yaml" + // List Containers // swagger:response ListContainers type swagInspectPodResponse struct { // in:body Body []ListContainer } + +// Inspect Manifest +// swagger:response InspectManifest +type swagInspectManifestResponse struct { + // in:body + Body manifest.List +} + +// Kill Pod +// swagger:response PodKillReport +type swagKillPodResponse struct { + // in:body + Body entities.PodKillReport +} + +// Pause pod +// swagger:response PodPauseReport +type swagPausePodResponse struct { + // in:body + Body entities.PodPauseReport +} + +// Unpause pod +// swagger:response PodUnpauseReport +type swagUnpausePodResponse struct { + // in:body + Body entities.PodUnpauseReport +} + +// Stop pod +// swagger:response PodStopReport +type swagStopPodResponse struct { + // in:body + Body entities.PodStopReport +} + +// Restart pod +// swagger:response PodRestartReport +type swagRestartPodResponse struct { + // in:body + Body entities.PodRestartReport +} + +// Start pod +// swagger:response PodStartReport +type swagStartPodResponse struct { + // in:body + Body entities.PodStartReport +} + +// Rm pod +// swagger:response PodRmReport +type swagRmPodResponse struct { + // in:body + Body entities.PodRmReport +} + +func ServeSwagger(w http.ResponseWriter, r *http.Request) { + path := DefaultPodmanSwaggerSpec + if p, found := os.LookupEnv("PODMAN_SWAGGER_SPEC"); found { + path = p + } + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + utils.InternalServerError(w, errors.Errorf("file %q does not exist", path)) + return + } + utils.InternalServerError(w, err) + return + } + w.Header().Set("Content-Type", "text/yaml") + http.ServeFile(w, r, path) +} diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 7e7e46718..5a6fc021e 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -6,11 +6,12 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/filters" "github.com/gorilla/schema" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) func CreateVolume(w http.ResponseWriter, r *http.Request) { @@ -23,13 +24,12 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { }{ // override any golang type defaults } - input := handlers.VolumeCreateConfig{} + input := entities.VolumeCreateOptions{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - // decode params from body if err := json.NewDecoder(r.Body).Decode(&input); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) @@ -45,18 +45,35 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { if len(input.Label) > 0 { volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) } - if len(input.Opts) > 0 { - parsedOptions, err := shared.ParseVolumeOptions(input.Opts) + if len(input.Options) > 0 { + parsedOptions, err := shared.ParseVolumeOptions(input.Options) if err != nil { utils.InternalServerError(w, err) + return } volumeOptions = append(volumeOptions, parsedOptions...) } vol, err := runtime.NewVolume(r.Context(), volumeOptions...) if err != nil { utils.InternalServerError(w, err) + return + } + config, err := vol.Config() + if err != nil { + utils.InternalServerError(w, err) + return } - utils.WriteResponse(w, http.StatusOK, vol.Name()) + volResponse := entities.VolumeConfigResponse{ + Name: config.Name, + Driver: config.Driver, + Mountpoint: config.MountPoint, + CreatedAt: config.CreatedTime, + Labels: config.Labels, + Options: config.Options, + UID: config.UID, + GID: config.GID, + } + utils.WriteResponse(w, http.StatusOK, volResponse) } func InspectVolume(w http.ResponseWriter, r *http.Request) { @@ -67,50 +84,85 @@ func InspectVolume(w http.ResponseWriter, r *http.Request) { vol, err := runtime.GetVolume(name) if err != nil { utils.VolumeNotFound(w, name, err) + return } - inspect, err := vol.Inspect() - if err != nil { - utils.InternalServerError(w, err) + volResponse := entities.VolumeConfigResponse{ + Name: vol.Name(), + Driver: vol.Driver(), + Mountpoint: vol.MountPoint(), + CreatedAt: vol.CreatedTime(), + Labels: vol.Labels(), + Scope: vol.Scope(), + Options: vol.Options(), + UID: vol.UID(), + GID: vol.GID(), } - utils.WriteResponse(w, http.StatusOK, inspect) + utils.WriteResponse(w, http.StatusOK, volResponse) } func ListVolumes(w http.ResponseWriter, r *http.Request) { - //var ( - // runtime = r.Context().Value("runtime").(*libpod.Runtime) - // decoder = r.Context().Value("decoder").(*schema.Decoder) - //) - //query := struct { - // Filter string `json:"filter"` - //}{ - // // override any golang type defaults - //} - // - //if err := decoder.Decode(&query, r.URL.Query()); err != nil { - // utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - // errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - // return - //} - /* - This is all in main in cmd and needs to be extracted from there first. - */ + var ( + decoder = r.Context().Value("decoder").(*schema.Decoder) + runtime = r.Context().Value("runtime").(*libpod.Runtime) + volumeConfigs []*entities.VolumeListReport + ) + query := struct { + Filters map[string][]string `schema:"filters"` + }{ + // override any golang type defaults + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + volumeFilters, err := filters.GenerateVolumeFilters(query.Filters) + if err != nil { + utils.InternalServerError(w, err) + return + } + + vols, err := runtime.Volumes(volumeFilters...) + if err != nil { + utils.InternalServerError(w, err) + return + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + volumeConfigs = append(volumeConfigs, &entities.VolumeListReport{VolumeConfigResponse: config}) + } + utils.WriteResponse(w, http.StatusOK, volumeConfigs) } func PruneVolumes(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) + reports []*entities.VolumePruneReport ) - pruned, errs := runtime.PruneVolumes(r.Context()) - if errs != nil { - if len(errs) > 1 { - for _, err := range errs { - log.Infof("Request Failed(%s): %s", http.StatusText(http.StatusInternalServerError), err.Error()) - } - } - utils.InternalServerError(w, errs[len(errs)-1]) + pruned, err := runtime.PruneVolumes(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return + } + for k, v := range pruned { + reports = append(reports, &entities.VolumePruneReport{ + Err: v, + Id: k, + }) } - utils.WriteResponse(w, http.StatusOK, pruned) + utils.WriteResponse(w, http.StatusOK, reports) } func RemoveVolume(w http.ResponseWriter, r *http.Request) { @@ -133,9 +185,15 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { vol, err := runtime.LookupVolume(name) if err != nil { utils.VolumeNotFound(w, name, err) + return } if err := runtime.RemoveVolume(r.Context(), vol, query.Force); err != nil { + if errors.Cause(err) == define.ErrVolumeBeingUsed { + utils.Error(w, "volumes being used", http.StatusConflict, err) + return + } utils.InternalServerError(w, err) + return } utils.WriteResponse(w, http.StatusNoContent, "") } diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index 4ba123ba9..e6e937729 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -2,7 +2,9 @@ package handlers import ( "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/inspect" "github.com/docker/docker/api/types" ) @@ -108,7 +110,7 @@ type swagDockerTopResponse struct { type swagLibpodInspectContainerResponse struct { // in:body Body struct { - libpod.InspectContainerData + define.InspectContainerData } } @@ -116,7 +118,7 @@ type swagLibpodInspectContainerResponse struct { // swagger:response ListPodsResponse type swagListPodsResponse struct { // in:body - Body []libpod.PodInspect + Body []entities.ListPodsReport } // Inspect pod diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index c72b0f817..1ca5db3f9 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -13,6 +13,7 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" libpodImage "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" docker "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" dockerEvents "github.com/docker/docker/api/types/events" @@ -45,12 +46,6 @@ type LibpodImagesPullReport struct { ID string `json:"id"` } -type ImageSummary struct { - docker.ImageSummary - CreatedTime time.Time `json:"CreatedTime,omitempty"` - ReadOnly bool `json:"ReadOnly,omitempty"` -} - type ContainersPruneReport struct { docker.ContainersPruneReport } @@ -128,14 +123,9 @@ type CreateContainerConfig struct { NetworkingConfig dockerNetwork.NetworkingConfig } -type VolumeCreateConfig struct { - Name string `json:"name"` - Driver string `schema:"driver"` - Label map[string]string `schema:"label"` - Opts map[string]string `schema:"opts"` -} - +// swagger:model IDResponse type IDResponse struct { + // ID ID string `json:"id"` } @@ -143,19 +133,6 @@ type ContainerTopOKBody struct { dockerContainer.ContainerTopOKBody } -// swagger:model PodCreateConfig -type PodCreateConfig struct { - Name string `json:"name"` - CGroupParent string `json:"cgroup-parent"` - Hostname string `json:"hostname"` - Infra bool `json:"infra"` - InfraCommand string `json:"infra-command"` - InfraImage string `json:"infra-image"` - Labels []string `json:"labels"` - Publish []string `json:"publish"` - Share string `json:"share"` -} - type ErrorModel struct { Message string `json:"message"` } @@ -182,6 +159,14 @@ type ImageTreeResponse struct { Layers []ImageLayer `json:"layers"` } +type ExecCreateConfig struct { + docker.ExecConfig +} + +type ExecCreateResponse struct { + docker.IDResponse +} + func EventToApiEvent(e *events.Event) *Event { return &Event{dockerEvents.Message{ Type: e.Type.String(), @@ -200,23 +185,13 @@ func EventToApiEvent(e *events.Event) *Event { }} } -func ImageToImageSummary(l *libpodImage.Image) (*ImageSummary, error) { +func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { containers, err := l.Containers() if err != nil { return nil, errors.Wrapf(err, "Failed to obtain Containers for image %s", l.ID()) } containerCount := len(containers) - var digests []string - for _, d := range l.Digests() { - digests = append(digests, string(d)) - } - - tags, err := l.RepoTags() - if err != nil { - return nil, errors.Wrapf(err, "Failed to obtain RepoTags for image %s", l.ID()) - } - // FIXME: GetParent() panics // parent, err := l.GetParent(context.TODO()) // if err != nil { @@ -232,20 +207,43 @@ func ImageToImageSummary(l *libpodImage.Image) (*ImageSummary, error) { if err != nil { return nil, errors.Wrapf(err, "Failed to obtain Size for image %s", l.ID()) } - dockerSummary := docker.ImageSummary{ - Containers: int64(containerCount), - Created: l.Created().Unix(), - ID: l.ID(), - Labels: labels, - ParentID: l.Parent, - RepoDigests: digests, - RepoTags: tags, - SharedSize: 0, - Size: int64(*size), - VirtualSize: int64(*size), - } - is := ImageSummary{ - ImageSummary: dockerSummary, + + repoTags, err := l.RepoTags() + if err != nil { + return nil, errors.Wrapf(err, "Failed to obtain RepoTags for image %s", l.ID()) + } + + history, err := l.History(context.TODO()) + if err != nil { + return nil, errors.Wrapf(err, "Failed to obtain History for image %s", l.ID()) + } + historyIds := make([]string, len(history)) + for i, h := range history { + historyIds[i] = h.ID + } + + digests := make([]string, len(l.Digests())) + for i, d := range l.Digests() { + digests[i] = string(d) + } + + is := entities.ImageSummary{ + ID: l.ID(), + ParentId: l.Parent, + RepoTags: repoTags, + Created: l.Created().Unix(), + Size: int64(*size), + SharedSize: 0, + VirtualSize: l.VirtualSize, + Labels: labels, + Containers: containerCount, + ReadOnly: l.IsReadOnly(), + Dangling: l.Dangling(), + Names: l.Names(), + Digest: string(l.Digest()), + Digests: digests, + ConfigDigest: string(l.ConfigDigest), + History: historyIds, } return &is, nil } @@ -342,7 +340,7 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI } -func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Container, error) { +func LibpodToContainer(l *libpod.Container, infoData []define.InfoData, sz bool) (*Container, error) { imageId, imageName := l.Image() var ( @@ -355,11 +353,18 @@ func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Contai if state, err = l.State(); err != nil { return nil, err } - if sizeRW, err = l.RWSize(); err != nil { - return nil, err + stateStr := state.String() + if stateStr == "configured" { + stateStr = "created" } - if sizeRootFs, err = l.RootFsSize(); err != nil { - return nil, err + + if sz { + if sizeRW, err = l.RWSize(); err != nil { + return nil, err + } + if sizeRootFs, err = l.RootFsSize(); err != nil { + return nil, err + } } return &Container{docker.Container{ @@ -373,7 +378,7 @@ func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Contai SizeRw: sizeRW, SizeRootFs: sizeRootFs, Labels: l.Labels(), - State: string(state), + State: stateStr, Status: "", HostConfig: struct { NetworkMode string `json:",omitempty"` @@ -386,9 +391,9 @@ func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Contai }, nil } -func LibpodToContainerJSON(l *libpod.Container) (*docker.ContainerJSON, error) { +func LibpodToContainerJSON(l *libpod.Container, sz bool) (*docker.ContainerJSON, error) { _, imageName := l.Image() - inspect, err := l.Inspect(true) + inspect, err := l.Inspect(sz) if err != nil { return nil, err } diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index 402005581..bbe4cee3c 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -2,9 +2,7 @@ package utils import ( "context" - "fmt" "net/http" - "syscall" "time" "github.com/containers/libpod/cmd/podman/shared" @@ -18,69 +16,18 @@ import ( // ContainerCreateResponse is the response struct for creating a container type ContainerCreateResponse struct { // ID of the container created - ID string `json:"id"` + ID string `json:"Id"` // Warnings during container creation Warnings []string `json:"Warnings"` } -func KillContainer(w http.ResponseWriter, r *http.Request) (*libpod.Container, error) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - query := struct { - Signal syscall.Signal `schema:"signal"` - }{ - Signal: syscall.SIGKILL, - } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return nil, err - } - name := GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - ContainerNotFound(w, name, err) - return nil, err - } - - state, err := con.State() - if err != nil { - InternalServerError(w, err) - return con, err - } - - // If the Container is stopped already, send a 409 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - Error(w, fmt.Sprintf("Container %s is not running", name), http.StatusConflict, errors.New(fmt.Sprintf("Cannot kill Container %s, it is not running", name))) - return con, err - } - - err = con.Kill(uint(query.Signal)) - if err != nil { - Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "unable to kill Container %s", name)) - } - return con, err -} - -func RemoveContainer(w http.ResponseWriter, r *http.Request, force, vols bool) { - runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := GetName(r) - con, err := runtime.LookupContainer(name) - if err != nil { - ContainerNotFound(w, name, err) - return - } - - if err := runtime.RemoveContainer(r.Context(), con, force, vols); err != nil { - InternalServerError(w, err) - return - } - WriteResponse(w, http.StatusNoContent, "") -} - func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { + var ( + err error + interval time.Duration + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) - // /{version}/containers/(name)/restart query := struct { Interval string `schema:"interval"` Condition string `schema:"condition"` @@ -91,25 +38,34 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return 0, err } - - if len(query.Condition) > 0 { - UnSupportedParameter("condition") + if _, found := r.URL.Query()["interval"]; found { + interval, err = time.ParseDuration(query.Interval) + if err != nil { + InternalServerError(w, err) + return 0, err + } + } else { + interval, err = time.ParseDuration("250ms") + if err != nil { + InternalServerError(w, err) + return 0, err + } + } + condition := define.ContainerStateStopped + if _, found := r.URL.Query()["condition"]; found { + condition, err = define.StringToContainerStatus(query.Condition) + if err != nil { + InternalServerError(w, err) + return 0, err + } } - name := GetName(r) con, err := runtime.LookupContainer(name) if err != nil { ContainerNotFound(w, name, err) return 0, err } - if len(query.Interval) > 0 { - d, err := time.ParseDuration(query.Interval) - if err != nil { - Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %s for interval", query.Interval)) - } - return con.WaitWithInterval(d) - } - return con.Wait() + return con.WaitForConditionWithInterval(interval, condition) } // GenerateFilterFuncsFromMap is used to generate un-executed functions that can be used to filter diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 44bcc794c..32b8c5b0a 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -3,7 +3,6 @@ package utils import ( "encoding/json" "fmt" - "github.com/pkg/errors" "io" "net/http" "net/url" @@ -11,6 +10,7 @@ import ( "strings" "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index f68a71561..696d5f745 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -6,7 +6,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" - "github.com/gorilla/mux" "github.com/gorilla/schema" ) @@ -28,7 +27,7 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { return nil, err } var filters = []string{} - if _, found := mux.Vars(r)["digests"]; found && query.Digests { + if _, found := r.URL.Query()["digests"]; found && query.Digests { UnSupportedParameter("digests") } @@ -44,3 +43,8 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { } } + +func GetImage(r *http.Request, name string) (*image.Image, error) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + return runtime.ImageRuntime().NewFromLocal(name) +} diff --git a/pkg/api/handlers/utils/pods.go b/pkg/api/handlers/utils/pods.go new file mode 100644 index 000000000..79d1a5090 --- /dev/null +++ b/pkg/api/handlers/utils/pods.go @@ -0,0 +1,84 @@ +package utils + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/gorilla/schema" +) + +func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport, error) { + var ( + lps []*entities.ListPodsReport + pods []*libpod.Pod + podErr error + ) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + All bool + Filters map[string][]string `schema:"filters"` + Digests bool + }{} + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + return nil, err + } + var filters = []string{} + if _, found := r.URL.Query()["digests"]; found && query.Digests { + UnSupportedParameter("digests") + } + + if len(query.Filters) > 0 { + for k, v := range query.Filters { + for _, val := range v { + filters = append(filters, fmt.Sprintf("%s=%s", k, val)) + } + } + filterFuncs, err := shared.GenerateFilterFunction(runtime, filters) + if err != nil { + return nil, err + } + pods, podErr = shared.FilterAllPodsWithFilterFunc(runtime, filterFuncs...) + } else { + pods, podErr = runtime.GetAllPods() + } + if podErr != nil { + return nil, podErr + } + for _, pod := range pods { + status, err := pod.GetPodStatus() + if err != nil { + return nil, err + } + ctrs, err := pod.AllContainers() + if err != nil { + return nil, err + } + lp := entities.ListPodsReport{ + Cgroup: pod.CgroupParent(), + Created: pod.CreatedTime(), + Id: pod.ID(), + Name: pod.Name(), + Namespace: pod.Namespace(), + Status: status, + } + for _, ctr := range ctrs { + state, err := ctr.State() + if err != nil { + return nil, err + } + lp.Containers = append(lp.Containers, &entities.ListPodContainer{ + Id: ctr.ID(), + Names: ctr.Name(), + Status: state.String(), + }) + } + lps = append(lps, &lp) + } + return lps, nil +} diff --git a/pkg/api/server/register_auth.go b/pkg/api/server/register_auth.go index 8db131153..33b707fa4 100644 --- a/pkg/api/server/register_auth.go +++ b/pkg/api/server/register_auth.go @@ -1,11 +1,13 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerAuthHandlers(r *mux.Router) error { - r.Handle(VersionedPath("/auth"), s.APIHandler(handlers.UnsupportedHandler)) + r.Handle(VersionedPath("/auth"), s.APIHandler(compat.UnsupportedHandler)) + // Added non version path to URI to support docker non versioned paths + r.Handle("/auth", s.APIHandler(compat.UnsupportedHandler)) return nil } diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 6a2ba4b1e..2656d1d89 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -3,8 +3,7 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/gorilla/mux" ) @@ -33,7 +32,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/create"), s.APIHandler(generic.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/create"), s.APIHandler(compat.CreateContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/create", s.APIHandler(compat.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/json compat listContainers // --- // tags: @@ -83,7 +84,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/BadParamError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/json"), s.APIHandler(generic.ListContainers)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/containers/json"), s.APIHandler(compat.ListContainers)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/json", s.APIHandler(compat.ListContainers)).Methods(http.MethodGet) // swagger:operation POST /containers/prune compat pruneContainers // --- // tags: @@ -105,7 +108,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/DocsContainerPruneReport" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/prune"), s.APIHandler(handlers.PruneContainers)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/prune"), s.APIHandler(compat.PruneContainers)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/prune", s.APIHandler(compat.PruneContainers)).Methods(http.MethodPost) // swagger:operation DELETE /containers/{name} compat removeContainer // --- // tags: @@ -144,7 +149,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}"), s.APIHandler(generic.RemoveContainer)).Methods(http.MethodDelete) + r.HandleFunc(VersionedPath("/containers/{name}"), s.APIHandler(compat.RemoveContainer)).Methods(http.MethodDelete) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}", s.APIHandler(compat.RemoveContainer)).Methods(http.MethodDelete) // swagger:operation GET /containers/{name}/json compat getContainer // --- // tags: @@ -171,7 +178,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/json"), s.APIHandler(generic.GetContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/containers/{name}/json"), s.APIHandler(compat.GetContainer)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/json", s.APIHandler(compat.GetContainer)).Methods(http.MethodGet) // swagger:operation POST /containers/{name}/kill compat killContainer // --- // tags: @@ -201,7 +210,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/kill"), s.APIHandler(generic.KillContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/kill"), s.APIHandler(compat.KillContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/kill", s.APIHandler(compat.KillContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/{name}/logs compat logsFromContainer // --- // tags: @@ -253,7 +264,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/logs"), s.APIHandler(generic.LogsFromContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/containers/{name}/logs"), s.APIHandler(compat.LogsFromContainer)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/logs", s.APIHandler(compat.LogsFromContainer)).Methods(http.MethodGet) // swagger:operation POST /containers/{name}/pause compat pauseContainer // --- // tags: @@ -275,8 +288,12 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/pause"), s.APIHandler(handlers.PauseContainer)).Methods(http.MethodPost) - r.HandleFunc(VersionedPath("/containers/{name}/rename"), s.APIHandler(handlers.UnsupportedHandler)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/pause"), s.APIHandler(compat.PauseContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/pause", s.APIHandler(compat.PauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/rename"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/rename", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) // swagger:operation POST /containers/{name}/restart compat restartContainer // --- // tags: @@ -301,7 +318,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/restart"), s.APIHandler(handlers.RestartContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/restart"), s.APIHandler(compat.RestartContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/restart", s.APIHandler(compat.RestartContainer)).Methods(http.MethodPost) // swagger:operation POST /containers/{name}/start compat startContainer // --- // tags: @@ -329,7 +348,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/start"), s.APIHandler(handlers.StartContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/start"), s.APIHandler(compat.StartContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/start", s.APIHandler(compat.StartContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/{name}/stats compat statsContainer // --- // tags: @@ -356,7 +377,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/stats"), s.APIHandler(generic.StatsContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/containers/{name}/stats"), s.APIHandler(compat.StatsContainer)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/stats", s.APIHandler(compat.StatsContainer)).Methods(http.MethodGet) // swagger:operation POST /containers/{name}/stop compat stopContainer // --- // tags: @@ -384,7 +407,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/stop"), s.APIHandler(handlers.StopContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/stop"), s.APIHandler(compat.StopContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/stop", s.APIHandler(compat.StopContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/{name}/top compat topContainer // --- // tags: @@ -409,7 +434,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/top"), s.APIHandler(handlers.TopContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/containers/{name}/top"), s.APIHandler(compat.TopContainer)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/top", s.APIHandler(compat.TopContainer)).Methods(http.MethodGet) // swagger:operation POST /containers/{name}/unpause compat unpauseContainer // --- // tags: @@ -431,13 +458,15 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/unpause"), s.APIHandler(handlers.UnpauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/unpause"), s.APIHandler(compat.UnpauseContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/unpause", s.APIHandler(compat.UnpauseContainer)).Methods(http.MethodPost) // swagger:operation POST /containers/{name}/wait compat waitContainer // --- // tags: // - containers (compat) - // summary: Wait on a container to exit - // description: Block until a container stops, then returns the exit code. + // summary: Wait on a container + // description: Block until a container stops or given condition is met. // parameters: // - in: path // name: name @@ -447,7 +476,14 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // - in: query // name: condition // type: string - // description: not supported + // description: | + // wait until container is to a given condition. default is stopped. valid conditions are: + // - configured + // - created + // - exited + // - paused + // - running + // - stopped // produces: // - application/json // responses: @@ -457,7 +493,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/wait"), s.APIHandler(generic.WaitContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/wait"), s.APIHandler(compat.WaitContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/wait", s.APIHandler(compat.WaitContainer)).Methods(http.MethodPost) // swagger:operation POST /containers/{name}/attach compat attachContainer // --- // tags: @@ -512,7 +550,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/attach"), s.APIHandler(handlers.AttachContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/attach"), s.APIHandler(compat.AttachContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/attach", s.APIHandler(compat.AttachContainer)).Methods(http.MethodPost) // swagger:operation POST /containers/{name}/resize compat resizeContainer // --- // tags: @@ -544,7 +584,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/resize"), s.APIHandler(handlers.ResizeContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/containers/{name}/resize", s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) /* libpod endpoints @@ -661,7 +703,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/DocsLibpodPruneResponse" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/prune"), s.APIHandler(handlers.PruneContainers)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/prune"), s.APIHandler(compat.PruneContainers)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/showmounted libpod libpodShowMountedContainers // --- // tags: @@ -713,7 +755,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}"), s.APIHandler(libpod.RemoveContainer)).Methods(http.MethodDelete) + r.HandleFunc(VersionedPath("/libpod/containers/{name}"), s.APIHandler(compat.RemoveContainer)).Methods(http.MethodDelete) // swagger:operation GET /libpod/containers/{name}/json libpod libpodGetContainer // --- // tags: @@ -768,7 +810,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/kill"), s.APIHandler(libpod.KillContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/kill"), s.APIHandler(compat.KillContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/mount libpod libpodMountContainer // --- // tags: @@ -868,7 +910,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/logs"), s.APIHandler(generic.LogsFromContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/logs"), s.APIHandler(compat.LogsFromContainer)).Methods(http.MethodGet) // swagger:operation POST /libpod/containers/{name}/pause libpod libpodPauseContainer // --- // tags: @@ -890,7 +932,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // "$ref": "#/responses/NoSuchContainer" // 500: // "$ref": "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/pause"), s.APIHandler(handlers.PauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/pause"), s.APIHandler(compat.PauseContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/restart libpod libpodRestartContainer // --- // tags: @@ -915,7 +957,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/restart"), s.APIHandler(handlers.RestartContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/restart"), s.APIHandler(compat.RestartContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/start libpod libpodStartContainer // --- // tags: @@ -943,7 +985,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/start"), s.APIHandler(handlers.StartContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/start"), s.APIHandler(compat.StartContainer)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/{name}/stats libpod libpodStatsContainer // --- // tags: @@ -970,7 +1012,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/stats"), s.APIHandler(generic.StatsContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/stats"), s.APIHandler(compat.StatsContainer)).Methods(http.MethodGet) // swagger:operation GET /libpod/containers/{name}/top libpod libpodTopContainer // --- // tags: @@ -1004,7 +1046,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/top"), s.APIHandler(handlers.TopContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/top"), s.APIHandler(compat.TopContainer)).Methods(http.MethodGet) // swagger:operation POST /libpod/containers/{name}/unpause libpod libpodUnpauseContainer // --- // tags: @@ -1025,23 +1067,35 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/unpause"), s.APIHandler(handlers.UnpauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/unpause"), s.APIHandler(compat.UnpauseContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/wait libpod libpodWaitContainer // --- // tags: // - containers - // summary: Wait on a container to exit + // summary: Wait on a container + // description: Wait on a container to met a given condition // parameters: // - in: path // name: name // type: string // required: true // description: the name or ID of the container + // - in: query + // name: condition + // type: string + // description: | + // wait until container is to a given condition. default is stopped. valid conditions are: + // - configured + // - created + // - exited + // - paused + // - running + // - stopped // produces: // - application/json // responses: - // 204: - // description: no error + // 200: + // $ref: "#/responses/ContainerWaitResponse" // 404: // $ref: "#/responses/NoSuchContainer" // 500: @@ -1095,7 +1149,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/stop"), s.APIHandler(handlers.StopContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/stop"), s.APIHandler(compat.StopContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/attach libpod libpodAttachContainer // --- // tags: @@ -1150,7 +1204,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/attach"), s.APIHandler(handlers.AttachContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/attach"), s.APIHandler(compat.AttachContainer)).Methods(http.MethodPost) // swagger:operation POST /libpod/containers/{name}/resize libpod libpodResizeContainer // --- // tags: @@ -1182,6 +1236,6 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/resize"), s.APIHandler(handlers.ResizeContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_distribution.go b/pkg/api/server/register_distribution.go index f03662224..89f69ea67 100644 --- a/pkg/api/server/register_distribution.go +++ b/pkg/api/server/register_distribution.go @@ -1,11 +1,13 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerDistributionHandlers(r *mux.Router) error { - r.HandleFunc(VersionedPath("/distribution/{name}/json"), handlers.UnsupportedHandler) + r.HandleFunc(VersionedPath("/distribution/{name}/json"), compat.UnsupportedHandler) + // Added non version path to URI to support docker non versioned paths + r.HandleFunc("/distribution/{name}/json", compat.UnsupportedHandler) return nil } diff --git a/pkg/api/server/register_events.go b/pkg/api/server/register_events.go index bc3b62662..e909303da 100644 --- a/pkg/api/server/register_events.go +++ b/pkg/api/server/register_events.go @@ -3,7 +3,7 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) @@ -11,8 +11,37 @@ func (s *APIServer) registerEventsHandlers(r *mux.Router) error { // swagger:operation GET /events system getEvents // --- // tags: + // - system (compat) + // summary: Get events + // description: Returns events filtered on query parameters + // produces: + // - application/json + // parameters: + // - name: since + // type: string + // in: query + // description: start streaming events from this time + // - name: until + // type: string + // in: query + // description: stop streaming events later than this + // - name: filters + // type: string + // in: query + // description: JSON encoded map[string][]string of constraints + // responses: + // 200: + // description: returns a string of json data describing an event + // 500: + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/events"), s.APIHandler(compat.GetEvents)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/events", s.APIHandler(compat.GetEvents)).Methods(http.MethodGet) + // swagger:operation GET /libpod/events system libpodGetEvents + // --- + // tags: // - system - // summary: Returns events filtered on query parameters + // summary: Get events // description: Returns events filtered on query parameters // produces: // - application/json @@ -34,6 +63,6 @@ func (s *APIServer) registerEventsHandlers(r *mux.Router) error { // description: returns a string of json data describing an event // 500: // "$ref": "#/responses/InternalError" - r.Handle(VersionedPath("/events"), s.APIHandler(handlers.GetEvents)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/events"), s.APIHandler(compat.GetEvents)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_exec.go b/pkg/api/server/register_exec.go index ad62de3f5..71fb50307 100644 --- a/pkg/api/server/register_exec.go +++ b/pkg/api/server/register_exec.go @@ -3,12 +3,12 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerExecHandlers(r *mux.Router) error { - // swagger:operation POST /containers/{name}/create compat createExec + // swagger:operation POST /containers/{name}/exec compat createExec // --- // tags: // - exec (compat) @@ -74,7 +74,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // description: container is paused // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/containers/{name}/create"), s.APIHandler(handlers.CreateExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/containers/{name}/exec"), s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/containers/{name}/exec", s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost) // swagger:operation POST /exec/{id}/start compat startExec // --- // tags: @@ -110,7 +112,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // description: container is stopped or paused // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/exec/{id}/start"), s.APIHandler(handlers.StartExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/exec/{id}/start"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/exec/{id}/start", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) // swagger:operation POST /exec/{id}/resize compat resizeExec // --- // tags: @@ -141,7 +145,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchExecInstance" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/exec/{id}/resize"), s.APIHandler(handlers.ResizeExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/exec/{id}/resize"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/exec/{id}/resize", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) // swagger:operation GET /exec/{id}/json compat inspectExec // --- // tags: @@ -163,13 +169,15 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchExecInstance" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/exec/{id}/json"), s.APIHandler(handlers.InspectExec)).Methods(http.MethodGet) + r.Handle(VersionedPath("/exec/{id}/json"), s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/exec/{id}/json", s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet) /* libpod api follows */ - // swagger:operation POST /libpod/containers/{name}/create libpod libpodCreateExec + // swagger:operation POST /libpod/containers/{name}/exec libpod libpodCreateExec // --- // tags: // - exec @@ -235,7 +243,7 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // description: container is paused // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/containers/{name}/create"), s.APIHandler(handlers.CreateExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/containers/{name}/exec"), s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost) // swagger:operation POST /libpod/exec/{id}/start libpod libpodStartExec // --- // tags: @@ -271,7 +279,7 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // description: container is stopped or paused // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/exec/{id}/start"), s.APIHandler(handlers.StartExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/exec/{id}/start"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) // swagger:operation POST /libpod/exec/{id}/resize libpod libpodResizeExec // --- // tags: @@ -302,7 +310,7 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchExecInstance" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/exec/{id}/resize"), s.APIHandler(handlers.ResizeExec)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/exec/{id}/resize"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) // swagger:operation GET /libpod/exec/{id}/json libpod libpodInspectExec // --- // tags: @@ -324,6 +332,6 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchExecInstance" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/libpod/exec/{id}/json"), s.APIHandler(handlers.InspectExec)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/exec/{id}/json"), s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_healthcheck.go b/pkg/api/server/register_healthcheck.go index 5466e2905..69aa5bbfb 100644 --- a/pkg/api/server/register_healthcheck.go +++ b/pkg/api/server/register_healthcheck.go @@ -8,6 +8,29 @@ import ( ) func (s *APIServer) registerHealthCheckHandlers(r *mux.Router) error { - r.Handle(VersionedPath("/libpod/containers/{name}/runhealthcheck"), s.APIHandler(libpod.RunHealthCheck)).Methods(http.MethodGet) + // swagger:operation GET /libpod/containers/{name:.*}/healthcheck libpod libpodRunHealthCheck + // --- + // tags: + // - containers + // summary: Run a container's healthcheck + // description: Execute the defined healthcheck and return information about the results + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/HealthcheckRun" + // 404: + // $ref: "#/responses/NoSuchContainer" + // 409: + // description: container has no healthcheck or is not running + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/containers/{name:.*}/healthcheck"), s.APIHandler(libpod.RunHealthCheck)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index db04ecdc9..e8dfe2fa8 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -3,8 +3,7 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/gorilla/mux" ) @@ -47,8 +46,12 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchImage" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/images/create"), s.APIHandler(generic.CreateImageFromImage)).Methods(http.MethodPost).Queries("fromImage", "{fromImage}") - r.Handle(VersionedPath("/images/create"), s.APIHandler(generic.CreateImageFromSrc)).Methods(http.MethodPost).Queries("fromSrc", "{fromSrc}") + r.Handle(VersionedPath("/images/create"), s.APIHandler(compat.CreateImageFromImage)).Methods(http.MethodPost).Queries("fromImage", "{fromImage}") + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/create", s.APIHandler(compat.CreateImageFromImage)).Methods(http.MethodPost).Queries("fromImage", "{fromImage}") + r.Handle(VersionedPath("/images/create"), s.APIHandler(compat.CreateImageFromSrc)).Methods(http.MethodPost).Queries("fromSrc", "{fromSrc}") + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/create", s.APIHandler(compat.CreateImageFromSrc)).Methods(http.MethodPost).Queries("fromSrc", "{fromSrc}") // swagger:operation GET /images/json compat listImages // --- // tags: @@ -83,7 +86,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/DockerImageSummary" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/json"), s.APIHandler(generic.GetImages)).Methods(http.MethodGet) + r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/json", s.APIHandler(compat.GetImages)).Methods(http.MethodGet) // swagger:operation POST /images/load compat importImage // --- // tags: @@ -107,7 +112,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: no error // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/load"), s.APIHandler(generic.LoadImages)).Methods(http.MethodPost) + r.Handle(VersionedPath("/images/load"), s.APIHandler(compat.LoadImages)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/load", s.APIHandler(compat.LoadImages)).Methods(http.MethodPost) // swagger:operation POST /images/prune compat pruneImages // --- // tags: @@ -132,7 +139,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/DocsImageDeleteResponse" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/prune"), s.APIHandler(generic.PruneImages)).Methods(http.MethodPost) + r.Handle(VersionedPath("/images/prune"), s.APIHandler(compat.PruneImages)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/prune", s.APIHandler(compat.PruneImages)).Methods(http.MethodPost) // swagger:operation GET /images/search compat searchImages // --- // tags: @@ -165,7 +174,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/search"), s.APIHandler(handlers.SearchImages)).Methods(http.MethodGet) + r.Handle(VersionedPath("/images/search"), s.APIHandler(compat.SearchImages)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/search", s.APIHandler(compat.SearchImages)).Methods(http.MethodGet) // swagger:operation DELETE /images/{name:.*} compat removeImage // --- // tags: @@ -197,7 +208,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/{name:.*}"), s.APIHandler(handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/images/{name:.*}"), s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/{name:.*}", s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /images/{name:.*}/get compat exportImage // --- // tags: @@ -220,7 +233,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // format: binary // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/{name:.*}/get"), s.APIHandler(generic.ExportImage)).Methods(http.MethodGet) + r.Handle(VersionedPath("/images/{name:.*}/get"), s.APIHandler(compat.ExportImage)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/{name:.*}/get", s.APIHandler(compat.ExportImage)).Methods(http.MethodGet) // swagger:operation GET /images/{name:.*}/history compat imageHistory // --- // tags: @@ -242,7 +257,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchImage" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/images/{name:.*}/history"), s.APIHandler(handlers.HistoryImage)).Methods(http.MethodGet) + r.Handle(VersionedPath("/images/{name:.*}/history"), s.APIHandler(compat.HistoryImage)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/{name:.*}/history", s.APIHandler(compat.HistoryImage)).Methods(http.MethodGet) // swagger:operation GET /images/{name:.*}/json compat inspectImage // --- // tags: @@ -264,7 +281,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchImage" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/images/{name:.*}/json"), s.APIHandler(generic.GetImage)).Methods(http.MethodGet) + r.Handle(VersionedPath("/images/{name:.*}/json"), s.APIHandler(compat.GetImage)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/{name:.*}/json", s.APIHandler(compat.GetImage)).Methods(http.MethodGet) // swagger:operation POST /images/{name:.*}/tag compat tagImage // --- // tags: @@ -298,7 +317,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/{name:.*}/tag"), s.APIHandler(handlers.TagImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/images/{name:.*}/tag"), s.APIHandler(compat.TagImage)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/images/{name:.*}/tag", s.APIHandler(compat.TagImage)).Methods(http.MethodPost) // swagger:operation POST /commit compat commitContainer // --- // tags: @@ -343,7 +364,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/NoSuchImage' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/commit"), s.APIHandler(generic.CommitContainer)).Methods(http.MethodPost) + r.Handle(VersionedPath("/commit"), s.APIHandler(compat.CommitContainer)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/commit", s.APIHandler(compat.CommitContainer)).Methods(http.MethodPost) // swagger:operation POST /build compat buildImage // --- @@ -553,7 +576,9 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/BadParamError" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/build"), s.APIHandler(handlers.BuildImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/build"), s.APIHandler(compat.BuildImage)).Methods(http.MethodPost) + // Added non version path to URI to support docker non versioned paths + r.Handle("/build", s.APIHandler(compat.BuildImage)).Methods(http.MethodPost) /* libpod endpoints */ @@ -627,7 +652,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/NoSuchImage' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/{name:.*}/history"), s.APIHandler(handlers.HistoryImage)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/images/{name:.*}/history"), s.APIHandler(compat.HistoryImage)).Methods(http.MethodGet) // swagger:operation GET /libpod/images/json libpod libpodListImages // --- // tags: @@ -822,7 +847,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/DocsSearchResponse" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(handlers.SearchImages)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(compat.SearchImages)).Methods(http.MethodGet) // swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage // --- // tags: @@ -852,7 +877,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/{name:.*}"), s.APIHandler(handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/libpod/images/{name:.*}"), s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /libpod/images/{name:.*}/get libpod libpodExportImage // --- // tags: @@ -941,7 +966,58 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/{name:.*}/tag"), s.APIHandler(handlers.TagImage)).Methods(http.MethodPost) - + r.Handle(VersionedPath("/libpod/images/{name:.*}/tag"), s.APIHandler(compat.TagImage)).Methods(http.MethodPost) + // swagger:operation POST /libpod/commit libpod libpodCommitContainer + // --- + // tags: + // - containers + // summary: Commit + // description: Create a new image from a container + // parameters: + // - in: query + // name: container + // type: string + // description: the name or ID of a container + // required: true + // - in: query + // name: repo + // type: string + // description: the repository name for the created image + // - in: query + // name: tag + // type: string + // description: tag name for the created image + // - in: query + // name: comment + // type: string + // description: commit message + // - in: query + // name: author + // type: string + // description: author of the image + // - in: query + // name: pause + // type: boolean + // description: pause the container before committing it + // - in: query + // name: changes + // description: instructions to apply while committing in Dockerfile format (i.e. "CMD=/bin/foo") + // type: array + // items: + // type: string + // - in: query + // name: format + // type: string + // description: format of the image manifest and metadata (default "oci") + // produces: + // - application/json + // responses: + // 201: + // description: no error + // 404: + // $ref: '#/responses/NoSuchImage' + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/commit"), s.APIHandler(libpod.CommitContainer)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_info.go b/pkg/api/server/register_info.go index 36c467cc3..b4ab8871c 100644 --- a/pkg/api/server/register_info.go +++ b/pkg/api/server/register_info.go @@ -3,7 +3,7 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) @@ -21,6 +21,8 @@ func (s *APIServer) registerInfoHandlers(r *mux.Router) error { // description: to be determined // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/info"), s.APIHandler(generic.GetInfo)).Methods(http.MethodGet) + r.Handle(VersionedPath("/info"), s.APIHandler(compat.GetInfo)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/info", s.APIHandler(compat.GetInfo)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_manifest.go b/pkg/api/server/register_manifest.go new file mode 100644 index 000000000..8fd84f205 --- /dev/null +++ b/pkg/api/server/register_manifest.go @@ -0,0 +1,145 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerManifestHandlers(r *mux.Router) error { + // swagger:operation POST /libpod/manifests/create manifests Create + // --- + // summary: Create + // description: Create a manifest list + // produces: + // - application/json + // parameters: + // - in: query + // name: name + // type: string + // description: manifest list name + // required: true + // - in: query + // name: image + // type: string + // description: name of the image + // - in: query + // name: all + // type: boolean + // description: add all contents if given list + // responses: + // 200: + // $ref: "#/definitions/IDResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: "#/responses/NoSuchImage" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/manifests/create"), s.APIHandler(libpod.ManifestCreate)).Methods(http.MethodPost) + // swagger:operation GET /libpod/manifests/{name:.*}/json manifests Inspect + // --- + // summary: Inspect + // description: Display a manifest list + // produces: + // - application/json + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: the name or ID of the manifest + // responses: + // 200: + // $ref: "#/responses/InspectManifest" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/manifests/{name:.*}/json"), s.APIHandler(libpod.ManifestInspect)).Methods(http.MethodGet) + // swagger:operation POST /libpod/manifests/{name:.*}/add manifests AddManifest + // --- + // description: Add an image to a manifest list + // produces: + // - application/json + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: the name or ID of the manifest + // - in: body + // name: options + // description: options for creating a manifest + // schema: + // $ref: "#/definitions/ManifestAddOpts" + // responses: + // 200: + // $ref: "#/definitions/IDResponse" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 409: + // $ref: "#/responses/BadParamError" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/manifests/{name:.*}/add"), s.APIHandler(libpod.ManifestAdd)).Methods(http.MethodPost) + // swagger:operation DELETE /libpod/manifests/{name:.*} manifests RemoveManifest + // --- + // summary: Remove + // description: Remove an image from a manifest list + // produces: + // - application/json + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: the image associated with the manifest + // - in: query + // name: digest + // type: string + // description: image digest to be removed + // responses: + // 200: + // $ref: "#/definitions/IDResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/manifests/{name:.*}"), s.APIHandler(libpod.ManifestRemove)).Methods(http.MethodDelete) + // swagger:operation POST /libpod/manifests/{name}/push manifests PushManifest + // --- + // summary: Push + // description: Push a manifest list or image index to a registry + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the manifest + // - in: query + // name: destination + // type: string + // required: true + // description: the destination for the manifest + // - in: query + // name: all + // description: push all images + // type: boolean + // responses: + // 200: + // $ref: "#/definitions/IDResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: "#/responses/NoSuchManifest" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/manifests/{name}/push"), s.APIHandler(libpod.ManifestPush)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/register_monitor.go b/pkg/api/server/register_monitor.go index dbe0d27ce..b7a7c3792 100644 --- a/pkg/api/server/register_monitor.go +++ b/pkg/api/server/register_monitor.go @@ -1,11 +1,13 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerMonitorHandlers(r *mux.Router) error { - r.Handle(VersionedPath("/monitor"), s.APIHandler(handlers.UnsupportedHandler)) + r.Handle(VersionedPath("/monitor"), s.APIHandler(compat.UnsupportedHandler)) + // Added non version path to URI to support docker non versioned paths + r.Handle("/monitor", s.APIHandler(compat.UnsupportedHandler)) return nil } diff --git a/pkg/api/server/register_ping.go b/pkg/api/server/register_ping.go index 349a8a71a..8a1cda3d4 100644 --- a/pkg/api/server/register_ping.go +++ b/pkg/api/server/register_ping.go @@ -3,14 +3,14 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerPingHandlers(r *mux.Router) error { - r.Handle("/_ping", s.APIHandler(handlers.Ping)).Methods(http.MethodGet) - r.Handle("/_ping", s.APIHandler(handlers.Ping)).Methods(http.MethodHead) + r.Handle("/_ping", s.APIHandler(compat.Ping)).Methods(http.MethodGet) + r.Handle("/_ping", s.APIHandler(compat.Ping)).Methods(http.MethodHead) // swagger:operation GET /libpod/_ping libpod libpodPingGet // --- @@ -61,7 +61,7 @@ func (s *APIServer) registerPingHandlers(r *mux.Router) error { // determine if talking to Podman engine or another engine // 500: // $ref: "#/responses/InternalError" - r.Handle("/libpod/_ping", s.APIHandler(handlers.Ping)).Methods(http.MethodGet) - r.Handle("/libpod/_ping", s.APIHandler(handlers.Ping)).Methods(http.MethodHead) + r.Handle("/libpod/_ping", s.APIHandler(compat.Ping)).Methods(http.MethodGet) + r.Handle("/libpod/_ping", s.APIHandler(compat.Ping)).Methods(http.MethodHead) return nil } diff --git a/pkg/api/server/register_plugins.go b/pkg/api/server/register_plugins.go index 479a79d1f..5f6473fe8 100644 --- a/pkg/api/server/register_plugins.go +++ b/pkg/api/server/register_plugins.go @@ -1,11 +1,13 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerPluginsHandlers(r *mux.Router) error { - r.Handle(VersionedPath("/plugins"), s.APIHandler(handlers.UnsupportedHandler)) + r.Handle(VersionedPath("/plugins"), s.APIHandler(compat.UnsupportedHandler)) + // Added non version path to URI to support docker non versioned paths + r.Handle("/plugins", s.APIHandler(compat.UnsupportedHandler)) return nil } diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index af2330665..5ba2263e8 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -37,7 +37,7 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // description: attributes for creating a pod // schema: // type: object - // $ref: "#/definitions/PodCreateConfig" + // $ref: "#/definitions/PodSpecGenerator" // responses: // 200: // $ref: "#/definitions/IdResponse" @@ -81,8 +81,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // type: boolean // description : force removal of a running pod by first stopping all containers, then removing all containers in the pod // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodRmReport' // 400: // $ref: "#/responses/BadParamError" // 404: @@ -146,8 +146,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // description: signal to be sent to pod // default: SIGKILL // responses: - // 204: - // description: no error + // 200: + // $ref: "#/responses/PodKillReport" // 400: // $ref: "#/responses/BadParamError" // 404: @@ -170,8 +170,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // required: true // description: the name or ID of the pod // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodPauseReport' // 404: // $ref: "#/responses/NoSuchPod" // 500: @@ -189,8 +189,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // required: true // description: the name or ID of the pod // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodRestartReport' // 404: // $ref: "#/responses/NoSuchPod" // 500: @@ -208,8 +208,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // required: true // description: the name or ID of the pod // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodStartReport' // 304: // $ref: "#/responses/PodAlreadyStartedError" // 404: @@ -233,8 +233,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // type: integer // description: timeout // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodStopReport' // 304: // $ref: "#/responses/PodAlreadyStoppedError" // 400: @@ -256,8 +256,8 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // required: true // description: the name or ID of the pod // responses: - // 204: - // description: no error + // 200: + // $ref: '#/responses/PodUnpauseReport' // 404: // $ref: "#/responses/NoSuchPod" // 500: diff --git a/pkg/api/server/register_swagger.go b/pkg/api/server/register_swagger.go index 5564ec096..9048c1951 100644 --- a/pkg/api/server/register_swagger.go +++ b/pkg/api/server/register_swagger.go @@ -2,25 +2,14 @@ package server import ( "net/http" - "os" + "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/gorilla/mux" ) -// DefaultPodmanSwaggerSpec provides the default path to the podman swagger spec file -const DefaultPodmanSwaggerSpec = "/usr/share/containers/podman/swagger.yaml" - // RegisterSwaggerHandlers maps the swagger endpoint for the server func (s *APIServer) RegisterSwaggerHandlers(r *mux.Router) error { // This handler does _*NOT*_ provide an UI rather just a swagger spec that an UI could render - r.PathPrefix("/swagger/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := DefaultPodmanSwaggerSpec - if p, found := os.LookupEnv("PODMAN_SWAGGER_SPEC"); found { - path = p - } - w.Header().Set("Content-Type", "text/yaml") - - http.ServeFile(w, r, path) - }) + r.HandleFunc(VersionedPath("/libpod/swagger"), s.APIHandler(libpod.ServeSwagger)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_swarm.go b/pkg/api/server/register_swarm.go index e37ac4e41..8a5588268 100644 --- a/pkg/api/server/register_swarm.go +++ b/pkg/api/server/register_swarm.go @@ -16,6 +16,14 @@ func (s *APIServer) registerSwarmHandlers(r *mux.Router) error { r.PathPrefix("/v{version:[0-9.]+}/services/").HandlerFunc(noSwarm) r.PathPrefix("/v{version:[0-9.]+}/swarm/").HandlerFunc(noSwarm) r.PathPrefix("/v{version:[0-9.]+}/tasks/").HandlerFunc(noSwarm) + + // Added non version path to URI to support docker non versioned paths + r.PathPrefix("/configs/").HandlerFunc(noSwarm) + r.PathPrefix("/nodes/").HandlerFunc(noSwarm) + r.PathPrefix("/secrets/").HandlerFunc(noSwarm) + r.PathPrefix("/services/").HandlerFunc(noSwarm) + r.PathPrefix("/swarm/").HandlerFunc(noSwarm) + r.PathPrefix("/tasks/").HandlerFunc(noSwarm) return nil } diff --git a/pkg/api/server/register_system.go b/pkg/api/server/register_system.go index 188c1cdac..708ccd39b 100644 --- a/pkg/api/server/register_system.go +++ b/pkg/api/server/register_system.go @@ -3,11 +3,13 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerSystemHandlers(r *mux.Router) error { - r.Handle(VersionedPath("/system/df"), s.APIHandler(generic.GetDiskUsage)).Methods(http.MethodGet) + r.Handle(VersionedPath("/system/df"), s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet) + // Added non version path to URI to support docker non versioned paths + r.Handle("/system/df", s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_version.go b/pkg/api/server/register_version.go index ee01ad4b3..25cacbc61 100644 --- a/pkg/api/server/register_version.go +++ b/pkg/api/server/register_version.go @@ -3,12 +3,12 @@ package server import ( "net/http" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/compat" "github.com/gorilla/mux" ) func (s *APIServer) registerVersionHandlers(r *mux.Router) error { - r.Handle("/version", s.APIHandler(handlers.VersionHandler)).Methods(http.MethodGet) - r.Handle(VersionedPath("/version"), s.APIHandler(handlers.VersionHandler)).Methods(http.MethodGet) + r.Handle("/version", s.APIHandler(compat.VersionHandler)).Methods(http.MethodGet) + r.Handle(VersionedPath("/version"), s.APIHandler(compat.VersionHandler)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index efe56a3ad..93b972b6b 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -11,26 +11,53 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // swagger:operation POST /libpod/volumes/create volumes createVolume // --- // summary: Create a volume + // parameters: + // - in: body + // name: create + // description: attributes for creating a container + // schema: + // $ref: "#/definitions/VolumeCreate" // produces: // - application/json // responses: + // '201': + // $ref: "#/responses/VolumeCreateResponse" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/volumes/create"), s.APIHandler(libpod.CreateVolume)).Methods(http.MethodPost) + // swagger:operation GET /libpod/volumes/json volumes listVolumes + // --- + // summary: List volumes + // description: Returns a list of networks + // produces: + // - application/json + // parameters: + // - in: query + // name: filters + // type: string + // description: | + // JSON encoded value of the filters (a map[string][]string) to process on the networks list. Available filters: + // - driver=<volume-driver-name> Matches volumes based on their driver. + // - label=<key> or label=<key>:<value> Matches volumes based on the presence of a label alone or a label and a value. + // - name=<volume-name> Matches all of volume name. + // - opt=<driver-option> Matches a storage driver options + // responses: // '200': - // description: tbd + // "$ref": "#/responses/VolumeList" // '500': // "$ref": "#/responses/InternalError" - r.Handle("/libpod/volumes/create", s.APIHandler(libpod.CreateVolume)).Methods(http.MethodPost) - r.Handle("/libpod/volumes/json", s.APIHandler(libpod.ListVolumes)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/volumes/json"), s.APIHandler(libpod.ListVolumes)).Methods(http.MethodGet) // swagger:operation POST /libpod/volumes/prune volumes pruneVolumes // --- // summary: Prune volumes // produces: // - application/json // responses: - // '204': - // description: no error + // '200': + // "$ref": "#/responses/VolumePruneResponse" // '500': // "$ref": "#/responses/InternalError" - r.Handle("/libpod/volumes/prune", s.APIHandler(libpod.PruneVolumes)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/volumes/prune"), s.APIHandler(libpod.PruneVolumes)).Methods(http.MethodPost) // swagger:operation GET /libpod/volumes/{name}/json volumes inspectVolume // --- // summary: Inspect volume @@ -44,12 +71,12 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // - application/json // responses: // '200': - // "$ref": "#/responses/InspectVolumeResponse" + // "$ref": "#/responses/VolumeCreateResponse" // '404': - // "$ref": "#/responses/NoSuchVolume" + // "$ref": "#/responses/NoSuchVolume" // '500': - // "$ref": "#/responses/InternalError" - r.Handle("/libpod/volumes/{name}/json", s.APIHandler(libpod.InspectVolume)).Methods(http.MethodGet) + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/volumes/{name}/json"), s.APIHandler(libpod.InspectVolume)).Methods(http.MethodGet) // swagger:operation DELETE /libpod/volumes/{name} volumes removeVolume // --- // summary: Remove volume @@ -68,12 +95,12 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // responses: // 204: // description: no error - // 400: - // $ref: "#/responses/BadParamError" // 404: // $ref: "#/responses/NoSuchVolume" + // 409: + // description: Volume is in use and cannot be removed // 500: // $ref: "#/responses/InternalError" - r.Handle("/libpod/volumes/{name}", s.APIHandler(libpod.RemoveVolume)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/libpod/volumes/{name}"), s.APIHandler(libpod.RemoveVolume)).Methods(http.MethodDelete) return nil } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index a5922e5d7..59f1f95cb 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -12,7 +12,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers" - "github.com/coreos/go-systemd/activation" + "github.com/coreos/go-systemd/v22/activation" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -83,10 +83,6 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li ConnectionCh: make(chan int), } - server.Timer = time.AfterFunc(server.Duration, func() { - server.ConnectionCh <- NOOPHandler - }) - router.NotFoundHandler = http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // We can track user errors... @@ -99,15 +95,17 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li server.registerAuthHandlers, server.registerContainersHandlers, server.registerDistributionHandlers, - server.registerExecHandlers, server.registerEventsHandlers, + server.registerExecHandlers, server.registerHealthCheckHandlers, server.registerImagesHandlers, server.registerInfoHandlers, + server.registerManifestHandlers, server.registerMonitorHandlers, server.registerPingHandlers, server.registerPluginsHandlers, server.registerPodsHandlers, + server.RegisterSwaggerHandlers, server.registerSwarmHandlers, server.registerSystemHandlers, server.registerVersionHandlers, @@ -138,36 +136,15 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li // Serve starts responding to HTTP requests func (s *APIServer) Serve() error { - // stalker to count the connections. Should the timer expire it will shutdown the service. - go func() { - for delta := range s.ConnectionCh { - switch delta { - case EnterHandler: - s.Timer.Stop() - s.ActiveConnections += 1 - s.TotalConnections += 1 - case ExitHandler: - s.Timer.Stop() - s.ActiveConnections -= 1 - if s.ActiveConnections == 0 { - // Server will be shutdown iff the timer expires before being reset or stopped - s.Timer = time.AfterFunc(s.Duration, func() { - if err := s.Shutdown(); err != nil { - logrus.Errorf("Failed to shutdown APIServer: %v", err) - os.Exit(1) - } - }) - } else { - s.Timer.Reset(s.Duration) - } - case NOOPHandler: - // push the check out another duration... - s.Timer.Reset(s.Duration) - default: - logrus.Errorf("ConnectionCh received unsupported input %d", delta) - } - } - }() + // This is initialized here as Timer is not needed until Serve'ing + if s.Duration > 0 { + s.Timer = time.AfterFunc(s.Duration, func() { + s.ConnectionCh <- NOOPHandler + }) + go s.ReadChannelWithTimeout() + } else { + go s.ReadChannelNoTimeout() + } sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) @@ -192,6 +169,53 @@ func (s *APIServer) Serve() error { return nil } +func (s *APIServer) ReadChannelWithTimeout() { + // stalker to count the connections. Should the timer expire it will shutdown the service. + for delta := range s.ConnectionCh { + switch delta { + case EnterHandler: + s.Timer.Stop() + s.ActiveConnections += 1 + s.TotalConnections += 1 + case ExitHandler: + s.Timer.Stop() + s.ActiveConnections -= 1 + if s.ActiveConnections == 0 { + // Server will be shutdown iff the timer expires before being reset or stopped + s.Timer = time.AfterFunc(s.Duration, func() { + if err := s.Shutdown(); err != nil { + logrus.Errorf("Failed to shutdown APIServer: %v", err) + os.Exit(1) + } + }) + } else { + s.Timer.Reset(s.Duration) + } + case NOOPHandler: + // push the check out another duration... + s.Timer.Reset(s.Duration) + default: + logrus.Warnf("ConnectionCh received unsupported input %d", delta) + } + } +} + +func (s *APIServer) ReadChannelNoTimeout() { + // stalker to count the connections. + for delta := range s.ConnectionCh { + switch delta { + case EnterHandler: + s.ActiveConnections += 1 + s.TotalConnections += 1 + case ExitHandler: + s.ActiveConnections -= 1 + case NOOPHandler: + default: + logrus.Warnf("ConnectionCh received unsupported input %d", delta) + } + } +} + // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { // Duration == 0 flags no auto-shutdown of the server diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go index fc409d816..2433a6a05 100644 --- a/pkg/api/server/swagger.go +++ b/pkg/api/server/swagger.go @@ -1,8 +1,10 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" ) // No such image @@ -50,6 +52,15 @@ type swagErrNoSuchPod struct { } } +// No such manifest +// swagger:response NoSuchManifest +type swagErrNoSuchManifest struct { + // in:body + Body struct { + utils.ErrorModel + } +} + // Internal server error // swagger:response InternalError type swagInternalError struct { @@ -117,7 +128,7 @@ type swagPodAlreadyStopped struct { // swagger:response DockerImageSummary type swagImageSummary struct { // in:body - Body []handlers.ImageSummary + Body []entities.ImageSummary } // List Containers @@ -139,3 +150,35 @@ type ok struct { ok string } } + +// Volume prune response +// swagger:response VolumePruneResponse +type swagVolumePruneResponse struct { + // in:body + Body []entities.VolumePruneReport +} + +// Volume create response +// swagger:response VolumeCreateResponse +type swagVolumeCreateResponse struct { + // in:body + Body struct { + entities.VolumeConfigResponse + } +} + +// Volume list +// swagger:response VolumeList +type swagVolumeListResponse struct { + // in:body + Body []libpod.Volume +} + +// Healthcheck +// swagger:response HealthcheckRun +type swagHealthCheckRunResponse struct { + // in:body + Body struct { + define.HealthCheckResults + } +} diff --git a/pkg/api/tags.yaml b/pkg/api/tags.yaml index 3bf2bb64f..5b5d9f5bb 100644 --- a/pkg/api/tags.yaml +++ b/pkg/api/tags.yaml @@ -6,6 +6,8 @@ tags: - name: images description: Actions related to images - name: pods + description: Actions related to manifests + - name: manifests description: Actions related to pods - name: volumes description: Actions related to volumes @@ -18,4 +20,4 @@ tags: - name: images (compat) description: Actions related to images for the compatibility endpoints - name: system (compat) - description: Actions related to Podman and compatiblity engines + description: Actions related to Podman and compatibility engines diff --git a/pkg/apparmor/apparmor.go b/pkg/apparmor/apparmor.go index 45c029c07..8e17361cb 100644 --- a/pkg/apparmor/apparmor.go +++ b/pkg/apparmor/apparmor.go @@ -2,14 +2,16 @@ package apparmor import ( "errors" + + "github.com/containers/common/pkg/config" libpodVersion "github.com/containers/libpod/version" ) var ( // DefaultLipodProfilePrefix is used for version-independent presence checks. - DefaultLipodProfilePrefix = "libpod-default" + "-" + DefaultLipodProfilePrefix = config.DefaultApparmorProfile // DefaultLibpodProfile is the name of default libpod AppArmor profile. - DefaultLibpodProfile = DefaultLipodProfilePrefix + libpodVersion.Version + DefaultLibpodProfile = DefaultLipodProfilePrefix + "-" + libpodVersion.Version // ErrApparmorUnsupported indicates that AppArmor support is not supported. ErrApparmorUnsupported = errors.New("AppArmor is not supported") // ErrApparmorRootless indicates that AppArmor support is not supported in rootless mode. diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go new file mode 100644 index 000000000..7c243eb00 --- /dev/null +++ b/pkg/autoupdate/autoupdate.go @@ -0,0 +1,280 @@ +package autoupdate + +import ( + "context" + "os" + "sort" + + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/systemd" + systemdGen "github.com/containers/libpod/pkg/systemd/generate" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Label denotes the container/pod label key to specify auto-update policies in +// container labels. +const Label = "io.containers.autoupdate" + +// Policy represents an auto-update policy. +type Policy string + +const ( + // PolicyDefault is the default policy denoting no auto updates. + PolicyDefault Policy = "disabled" + // PolicyNewImage is the policy to update as soon as there's a new image found. + PolicyNewImage = "image" +) + +// Map for easy lookups of supported policies. +var supportedPolicies = map[string]Policy{ + "": PolicyDefault, + "disabled": PolicyDefault, + "image": PolicyNewImage, +} + +// LookupPolicy looksup the corresponding Policy for the specified +// string. If none is found, an errors is returned including the list of +// supported policies. +// +// Note that an empty string resolved to PolicyDefault. +func LookupPolicy(s string) (Policy, error) { + policy, exists := supportedPolicies[s] + if exists { + return policy, nil + } + + // Sort the keys first as maps are non-deterministic. + keys := []string{} + for k := range supportedPolicies { + if k != "" { + keys = append(keys, k) + } + } + sort.Strings(keys) + + return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys) +} + +// ValidateImageReference checks if the specified imageName is a fully-qualified +// image reference to the docker transport (without digest). Such a reference +// includes a domain, name and tag (e.g., quay.io/podman/stable:latest). The +// reference may also be prefixed with "docker://" explicitly indicating that +// it's a reference to the docker transport. +func ValidateImageReference(imageName string) error { + // Make sure the input image is a docker. + imageRef, err := alltransports.ParseImageName(imageName) + if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { + return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name()) + } else if err != nil { + repo, err := reference.Parse(imageName) + if err != nil { + return errors.Wrap(err, "error enforcing fully-qualified docker transport reference for auto updates") + } + if _, ok := repo.(reference.NamedTagged); !ok { + return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName) + } + if _, ok := repo.(reference.Digested); ok { + return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName) + } + } + return nil +} + +// AutoUpdate looks up containers with a specified auto-update policy and acts +// accordingly. If the policy is set to PolicyNewImage, it checks if the image +// on the remote registry is different than the local one. If the image digests +// differ, it pulls the remote image and restarts the systemd unit running the +// container. +// +// It returns a slice of successfully restarted systemd units and a slice of +// errors encountered during auto update. +func AutoUpdate(runtime *libpod.Runtime) ([]string, []error) { + // Create a map from `image ID -> []*Container`. + containerMap, errs := imageContainersMap(runtime) + if len(containerMap) == 0 { + return nil, errs + } + + // Create a map from `image ID -> *image.Image` for image lookups. + imagesSlice, err := runtime.ImageRuntime().GetImages() + if err != nil { + return nil, []error{err} + } + imageMap := make(map[string]*image.Image) + for i := range imagesSlice { + imageMap[imagesSlice[i].ID()] = imagesSlice[i] + } + + // Connect to DBUS. + conn, err := systemd.ConnectToDBUS() + if err != nil { + logrus.Errorf(err.Error()) + return nil, []error{err} + } + defer conn.Close() + + // Update images. + containersToRestart := []*libpod.Container{} + updatedRawImages := make(map[string]bool) + for imageID, containers := range containerMap { + image, exists := imageMap[imageID] + if !exists { + errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID)) + return nil, errs + } + // Now we have to check if the image of any containers must be updated. + // Note that the image ID is NOT enough for this check as a given image + // may have multiple tags. + for i, ctr := range containers { + rawImageName := ctr.RawImageName() + if rawImageName == "" { + errs = append(errs, errors.Errorf("error auto-updating container %q: raw-image name is empty", ctr.ID())) + } + needsUpdate, err := newerImageAvailable(runtime, image, rawImageName) + if err != nil { + errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image check for %q failed", ctr.ID(), rawImageName)) + continue + } + if !needsUpdate { + continue + } + logrus.Infof("Auto-updating container %q using image %q", ctr.ID(), rawImageName) + if _, updated := updatedRawImages[rawImageName]; !updated { + _, err = updateImage(runtime, rawImageName) + if err != nil { + errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image update for %q failed", ctr.ID(), rawImageName)) + continue + } + updatedRawImages[rawImageName] = true + } + containersToRestart = append(containersToRestart, containers[i]) + } + } + + // Restart containers. + updatedUnits := []string{} + for _, ctr := range containersToRestart { + labels := ctr.Labels() + unit, exists := labels[systemdGen.EnvVariable] + if !exists { + // Shouldn't happen but let's be sure of it. + errs = append(errs, errors.Errorf("error auto-updating container %q: no %s label found", ctr.ID(), systemdGen.EnvVariable)) + continue + } + _, err := conn.RestartUnit(unit, "replace", nil) + if err != nil { + errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit)) + continue + } + logrus.Infof("Successfully restarted systemd unit %q", unit) + updatedUnits = append(updatedUnits, unit) + } + + return updatedUnits, errs +} + +// imageContainersMap generates a map[image ID] -> [containers using the image] +// of all containers with a valid auto-update policy. +func imageContainersMap(runtime *libpod.Runtime) (map[string][]*libpod.Container, []error) { + allContainers, err := runtime.GetAllContainers() + if err != nil { + return nil, []error{err} + } + + errors := []error{} + imageMap := make(map[string][]*libpod.Container) + for i, ctr := range allContainers { + state, err := ctr.State() + if err != nil { + errors = append(errors, err) + continue + } + // Only update running containers. + if state != define.ContainerStateRunning { + continue + } + // Only update containers with the specific label/policy set. + labels := ctr.Labels() + if value, exists := labels[Label]; exists { + policy, err := LookupPolicy(value) + if err != nil { + errors = append(errors, err) + continue + } + if policy != PolicyNewImage { + continue + } + } + // Now we know that `ctr` is configured for auto updates. + id, _ := ctr.Image() + imageMap[id] = append(imageMap[id], allContainers[i]) + } + + return imageMap, errors +} + +// newerImageAvailable returns true if there corresponding image on the remote +// registry is newer. +func newerImageAvailable(runtime *libpod.Runtime, img *image.Image, origName string) (bool, error) { + remoteRef, err := docker.ParseReference("//" + origName) + if err != nil { + return false, err + } + + remoteImg, err := remoteRef.NewImage(context.Background(), runtime.SystemContext()) + if err != nil { + return false, err + } + + rawManifest, _, err := remoteImg.Manifest(context.Background()) + if err != nil { + return false, err + } + + remoteDigest, err := manifest.Digest(rawManifest) + if err != nil { + return false, err + } + + return img.Digest().String() != remoteDigest.String(), nil +} + +// updateImage pulls the specified image. +func updateImage(runtime *libpod.Runtime, name string) (*image.Image, error) { + sys := runtime.SystemContext() + registryOpts := image.DockerRegistryOptions{} + signaturePolicyPath := "" + authFilePath := "" + + if sys != nil { + registryOpts.OSChoice = sys.OSChoice + registryOpts.ArchitectureChoice = sys.OSChoice + registryOpts.DockerCertPath = sys.DockerCertPath + + signaturePolicyPath = sys.SignaturePolicyPath + authFilePath = sys.AuthFilePath + } + + newImage, err := runtime.ImageRuntime().New(context.Background(), + docker.Transport.Name()+"://"+name, + signaturePolicyPath, + authFilePath, + os.Stderr, + ®istryOpts, + image.SigningOptions{}, + nil, + util.PullImageAlways, + ) + if err != nil { + return nil, err + } + return newImage, nil +} diff --git a/pkg/autoupdate/autoupdate_test.go b/pkg/autoupdate/autoupdate_test.go new file mode 100644 index 000000000..7a5da5bb0 --- /dev/null +++ b/pkg/autoupdate/autoupdate_test.go @@ -0,0 +1,50 @@ +package autoupdate + +import ( + "testing" +) + +func TestValidateImageReference(t *testing.T) { + tests := []struct { + input string + valid bool + }{ + { // Fully-qualified reference + input: "quay.io/foo/bar:tag", + valid: true, + }, + { // Fully-qualified reference in transport notation + input: "docker://quay.io/foo/bar:tag", + valid: true, + }, + { // Fully-qualified reference but with digest + input: "quay.io/foo/bar@sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9", + valid: false, + }, + { // Reference with missing tag + input: "quay.io/foo/bar", + valid: false, + }, + { // Short name + input: "alpine", + valid: false, + }, + { // Short name with repo + input: "library/alpine", + valid: false, + }, + { // Wrong transport + input: "docker-archive:/some/path.tar", + valid: false, + }, + } + + for _, test := range tests { + err := ValidateImageReference(test.input) + if test.valid && err != nil { + t.Fatalf("parsing %q should have succeeded: %v", test.input, err) + } else if !test.valid && err == nil { + t.Fatalf("parsing %q should have failed", test.input) + } + } +} diff --git a/pkg/bindings/bindings.go b/pkg/bindings/bindings.go index e83c4a5e1..4b07847d1 100644 --- a/pkg/bindings/bindings.go +++ b/pkg/bindings/bindings.go @@ -7,3 +7,12 @@ // is established, users can then manage the Podman container runtime. package bindings + +var ( + // PTrue is a convenience variable that can be used in bindings where + // a pointer to a bool (optional parameter) is required. + PTrue bool = true + // PFalse is a convenience variable that can be used in bindings where + // a pointer to a bool (optional parameter) is required. + PFalse bool = false +) diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index ba5f9c3aa..4fe4dd72d 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -109,7 +109,7 @@ func NewConnection(ctx context.Context, uri string, identity ...string) (context } client, err = tcpClient(_url) default: - return nil, errors.Errorf("%s is not a support schema", _url.Scheme) + return nil, errors.Errorf("'%s' is not a supported schema", _url.Scheme) } if err != nil { return nil, errors.Wrapf(err, "Failed to create %sClient", _url.Scheme) @@ -165,8 +165,13 @@ func sshClient(_url *url.URL, identity string, secure bool) (*http.Client, error } } + port := _url.Port() + if port == "" { + port = "22" + } + bastion, err := ssh.Dial("tcp", - net.JoinHostPort(_url.Hostname(), _url.Port()), + net.JoinHostPort(_url.Hostname(), port), &ssh.ClientConfig{ User: _url.User.Username(), Auth: []ssh.AuthMethod{auth}, diff --git a/pkg/bindings/containers/commit.go b/pkg/bindings/containers/commit.go new file mode 100644 index 000000000..12c25f842 --- /dev/null +++ b/pkg/bindings/containers/commit.go @@ -0,0 +1,49 @@ +package containers + +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" +) + +// Commit creates a container image from a container. The container is defined by nameOrId. Use +// the CommitOptions for finer grain control on characteristics of the resulting image. +func Commit(ctx context.Context, nameOrId string, options CommitOptions) (handlers.IDResponse, error) { + id := handlers.IDResponse{} + conn, err := bindings.GetClient(ctx) + if err != nil { + return id, err + } + params := url.Values{} + params.Set("container", nameOrId) + if options.Author != nil { + params.Set("author", *options.Author) + } + for _, change := range options.Changes { + params.Set("changes", change) + } + if options.Comment != nil { + params.Set("comment", *options.Comment) + } + if options.Format != nil { + params.Set("format", *options.Format) + } + if options.Pause != nil { + params.Set("pause", strconv.FormatBool(*options.Pause)) + } + if options.Repo != nil { + params.Set("repo", *options.Repo) + } + if options.Tag != nil { + params.Set("tag", *options.Tag) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/commit", params) + if err != nil { + return id, err + } + return id, response.Process(&id) +} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 2985787a6..bad1294f4 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -5,8 +5,10 @@ import ( "net/http" "net/url" "strconv" + "strings" - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers" lpapiv2 "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" ) @@ -105,7 +107,7 @@ func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { // or a partial/full ID. The size bool determines whether the size of the container's root filesystem // should be calculated. Calculating the size of a container requires extra work from the filesystem and // is therefore slower. -func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectContainerData, error) { +func Inspect(ctx context.Context, nameOrID string, size *bool) (*define.InspectContainerData, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err @@ -118,20 +120,20 @@ func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectC if err != nil { return nil, err } - inspect := libpod.InspectContainerData{} + inspect := define.InspectContainerData{} return &inspect, response.Process(&inspect) } // Kill sends a given signal to a given container. The signal should be the string // representation of a signal like 'SIGKILL'. The nameOrID can be a container name // or a partial/full ID -func Kill(ctx context.Context, nameOrID string, signal string) error { +func Kill(ctx context.Context, nameOrID string, sig string) error { conn, err := bindings.GetClient(ctx) if err != nil { return err } params := url.Values{} - params.Set("signal", signal) + params.Set("signal", sig) response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) if err != nil { return err @@ -139,7 +141,6 @@ func Kill(ctx context.Context, nameOrID string, signal string) error { return response.Process(nil) } -func Logs() {} // Pause pauses a given container. The nameOrID can be a container name // or a partial/full ID. @@ -194,7 +195,40 @@ func Start(ctx context.Context, nameOrID string, detachKeys *string) error { } func Stats() {} -func Top() {} + +// Top gathers statistics about the running processes in a container. The nameOrID can be a container name +// or a partial/full ID. The descriptors allow for specifying which data to collect from the process. +func Top(ctx context.Context, nameOrID string, descriptors []string) ([]string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + + if len(descriptors) > 0 { + // flatten the slice into one string + params.Set("ps_args", strings.Join(descriptors, ",")) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/top", params, nameOrID) + if err != nil { + return nil, err + } + + body := handlers.ContainerTopOKBody{} + if err = response.Process(&body); err != nil { + return nil, err + } + + // handlers.ContainerTopOKBody{} returns a slice of slices where each cell in the top table is an item. + // In libpod land, we're just using a slice with cells being split by tabs, which allows for an idiomatic + // usage of the tabwriter. + topOutput := []string{strings.Join(body.Titles, "\t")} + for _, out := range body.Processes { + topOutput = append(topOutput, strings.Join(out, "\t")) + } + + return topOutput, err +} // Unpause resumes the given paused container. The nameOrID can be a container name // or a partial/full ID. @@ -210,15 +244,20 @@ func Unpause(ctx context.Context, nameOrID string) error { return response.Process(nil) } -// Wait blocks until the given container exits and returns its exit code. The nameOrID can be a container name -// or a partial/full ID. -func Wait(ctx context.Context, nameOrID string) (int32, error) { +// Wait blocks until the given container reaches a condition. If not provided, the condition will +// default to stopped. If the condition is stopped, an exit code for the container will be provided. The +// nameOrID can be a container name or a partial/full ID. +func Wait(ctx context.Context, nameOrID string, condition *define.ContainerStatus) (int32, error) { //nolint var exitCode int32 conn, err := bindings.GetClient(ctx) if err != nil { return exitCode, err } - response, err := conn.DoRequest(nil, http.MethodPost, "containers/%s/wait", nil, nameOrID) + params := url.Values{} + if condition != nil { + params.Set("condition", condition.String()) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/wait", params, nameOrID) if err != nil { return exitCode, err } @@ -233,7 +272,7 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { if err != nil { return false, err } - response, err := conn.DoRequest(nil, http.MethodGet, "containers/%s/exists", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/exists", nil, nameOrID) if err != nil { return false, err } @@ -242,14 +281,14 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // Stop stops a running container. The timeout is optional. The nameOrID can be a container name // or a partial/full ID -func Stop(ctx context.Context, nameOrID string, timeout *int) error { +func Stop(ctx context.Context, nameOrID string, timeout *uint) error { params := url.Values{} conn, err := bindings.GetClient(ctx) if err != nil { return err } if timeout != nil { - params.Set("t", strconv.Itoa(*timeout)) + params.Set("t", strconv.Itoa(int(*timeout))) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) if err != nil { diff --git a/pkg/bindings/containers/create.go b/pkg/bindings/containers/create.go index 2943cb522..495f9db49 100644 --- a/pkg/bindings/containers/create.go +++ b/pkg/bindings/containers/create.go @@ -11,7 +11,7 @@ import ( jsoniter "github.com/json-iterator/go" ) -func CreateWithSpec(ctx context.Context, s specgen.SpecGenerator) (utils.ContainerCreateResponse, error) { +func CreateWithSpec(ctx context.Context, s *specgen.SpecGenerator) (utils.ContainerCreateResponse, error) { var ccr utils.ContainerCreateResponse conn, err := bindings.GetClient(ctx) if err != nil { @@ -19,7 +19,7 @@ func CreateWithSpec(ctx context.Context, s specgen.SpecGenerator) (utils.Contain } specgenString, err := jsoniter.MarshalToString(s) if err != nil { - return ccr, nil + return ccr, err } stringReader := strings.NewReader(specgenString) response, err := conn.DoRequest(stringReader, http.MethodPost, "/containers/create", nil) diff --git a/pkg/bindings/containers/exec.go b/pkg/bindings/containers/exec.go new file mode 100644 index 000000000..48f9ed697 --- /dev/null +++ b/pkg/bindings/containers/exec.go @@ -0,0 +1,71 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// ExecCreate creates a new exec session in an existing container. +// The exec session will not be started; that is done with ExecStart. +// Returns ID of new exec session, or an error if one occurred. +func ExecCreate(ctx context.Context, nameOrID string, config *handlers.ExecCreateConfig) (string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + + if config == nil { + return "", errors.Errorf("must provide a configuration for exec session") + } + + requestJSON, err := json.Marshal(config) + if err != nil { + return "", errors.Wrapf(err, "error marshalling exec config to JSON") + } + jsonReader := strings.NewReader(string(requestJSON)) + + resp, err := conn.DoRequest(jsonReader, http.MethodPost, "/containers/%s/exec", nil, nameOrID) + if err != nil { + return "", err + } + + respStruct := new(handlers.ExecCreateResponse) + if err := resp.Process(respStruct); err != nil { + return "", err + } + + return respStruct.ID, nil +} + +// ExecInspect inspects an existing exec session, returning detailed information +// about it. +func ExecInspect(ctx context.Context, sessionID string) (*define.InspectExecSession, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + + logrus.Debugf("Inspecting session ID %q", sessionID) + + resp, err := conn.DoRequest(nil, http.MethodGet, "/exec/%s/json", nil, sessionID) + if err != nil { + return nil, err + } + + respStruct := new(define.InspectExecSession) + if err := resp.Process(respStruct); err != nil { + return nil, err + } + + return respStruct, nil +} diff --git a/pkg/bindings/containers/healthcheck.go b/pkg/bindings/containers/healthcheck.go index dc607c1b3..2b783ac73 100644 --- a/pkg/bindings/containers/healthcheck.go +++ b/pkg/bindings/containers/healthcheck.go @@ -2,23 +2,23 @@ package containers import ( "context" - "github.com/containers/libpod/pkg/bindings" "net/http" - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/bindings" ) // RunHealthCheck executes the container's healthcheck and returns the health status of the // container. -func RunHealthCheck(ctx context.Context, nameOrID string) (*libpod.HealthCheckStatus, error) { +func RunHealthCheck(ctx context.Context, nameOrID string) (*define.HealthCheckResults, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } var ( - status libpod.HealthCheckStatus + status define.HealthCheckResults ) - response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/runhealthcheck", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/healthcheck", nil, nameOrID) if err != nil { return nil, err } diff --git a/pkg/bindings/containers/logs.go b/pkg/bindings/containers/logs.go new file mode 100644 index 000000000..b7ecb3c7e --- /dev/null +++ b/pkg/bindings/containers/logs.go @@ -0,0 +1,116 @@ +package containers + +import ( + "context" + "encoding/binary" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/bindings" + "github.com/pkg/errors" +) + +// Logs obtains a container's logs given the options provided. The logs are then sent to the +// stdout|stderr channels as strings. +func Logs(ctx context.Context, nameOrID string, opts LogOptions, stdoutChan, stderrChan chan string) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + params := url.Values{} + if opts.Follow != nil { + params.Set("follow", strconv.FormatBool(*opts.Follow)) + } + if opts.Since != nil { + params.Set("since", *opts.Since) + } + if opts.Stderr != nil { + params.Set("stderr", strconv.FormatBool(*opts.Stderr)) + } + if opts.Stdout != nil { + params.Set("stdout", strconv.FormatBool(*opts.Stdout)) + } + if opts.Tail != nil { + params.Set("tail", *opts.Tail) + } + if opts.Timestamps != nil { + params.Set("timestamps", strconv.FormatBool(*opts.Timestamps)) + } + if opts.Until != nil { + params.Set("until", *opts.Until) + } + // The API requires either stdout|stderr be used. If neither are specified, we specify stdout + if opts.Stdout == nil && opts.Stderr == nil { + params.Set("stdout", strconv.FormatBool(true)) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/logs", params, nameOrID) + if err != nil { + return err + } + + // read 8 bytes + // first byte determines stderr=2|stdout=1 + // bytes 4-7 len(msg) in uint32 + for { + stream, msgSize, err := readHeader(response.Body) + if err != nil { + // In case the server side closes up shop because !follow + if err == io.EOF { + break + } + return errors.Wrap(err, "unable to read log header") + } + msg, err := readMsg(response.Body, msgSize) + if err != nil { + return errors.Wrap(err, "unable to read log message") + } + if stream == 1 { + stdoutChan <- msg + } else { + stderrChan <- msg + } + } + return nil +} + +func readMsg(r io.Reader, msgSize int) (string, error) { + var msg []byte + size := msgSize + for { + b := make([]byte, size) + _, err := r.Read(b) + if err != nil { + return "", err + } + msg = append(msg, b...) + if len(msg) == msgSize { + break + } + size = msgSize - len(msg) + } + return string(msg), nil +} + +func readHeader(r io.Reader) (byte, int, error) { + var ( + header []byte + size = 8 + ) + for { + b := make([]byte, size) + _, err := r.Read(b) + if err != nil { + return 0, 0, err + } + header = append(header, b...) + if len(header) == 8 { + break + } + size = 8 - len(header) + } + stream := header[0] + msgSize := int(binary.BigEndian.Uint32(header[4:]) - 8) + return stream, msgSize, nil +} diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go new file mode 100644 index 000000000..31daaf565 --- /dev/null +++ b/pkg/bindings/containers/types.go @@ -0,0 +1,26 @@ +package containers + +// LogOptions describe finer control of log content or +// how the content is formatted. +type LogOptions struct { + Follow *bool + Since *string + Stderr *bool + Stdout *bool + Tail *string + Timestamps *bool + Until *string +} + +// CommitOptions describe details about the resulting commited +// image as defined by repo and tag. None of these options +// are required. +type CommitOptions struct { + Author *string + Changes []string + Comment *string + Format *string + Pause *bool + Repo *string + Tag *string +} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index c84aa4601..5e3af7a60 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -10,6 +10,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/inspect" ) @@ -29,8 +30,8 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // List returns a list of images in local storage. The all boolean and filters parameters are optional // ways to alter the image query. -func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handlers.ImageSummary, error) { - var imageSummary []*handlers.ImageSummary +func List(ctx context.Context, all *bool, filters map[string][]string) ([]*entities.ImageSummary, error) { + var imageSummary []*entities.ImageSummary conn, err := bindings.GetClient(ctx) if err != nil { return nil, err @@ -153,7 +154,7 @@ func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, c // Prune removes unused images from local storage. The optional filters can be used to further // define which images should be pruned. -func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { +func Prune(ctx context.Context, all *bool, filters map[string][]string) ([]string, error) { var ( deleted []string ) @@ -162,6 +163,9 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { return nil, err } params := url.Values{} + if all != nil { + params.Set("all", strconv.FormatBool(*all)) + } if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { @@ -173,7 +177,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { if err != nil { return deleted, err } - return deleted, response.Process(nil) + return deleted, response.Process(&deleted) } // Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required. diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go new file mode 100644 index 000000000..a8d1e6ca3 --- /dev/null +++ b/pkg/bindings/manifests/manifests.go @@ -0,0 +1,126 @@ +package manifests + +import ( + "context" + "errors" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + jsoniter "github.com/json-iterator/go" +) + +// Create creates a manifest for the given name. Optional images to be associated with +// the new manifest can also be specified. The all boolean specifies to add all entries +// of a list if the name provided is a manifest list. The ID of the new manifest list +// is returned as a string. +func Create(ctx context.Context, names, images []string, all *bool) (string, error) { + var idr handlers.IDResponse + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + if len(names) < 1 { + return "", errors.New("creating a manifest requires at least one name argument") + } + params := url.Values{} + if all != nil { + params.Set("all", strconv.FormatBool(*all)) + } + for _, name := range names { + params.Add("name", name) + } + for _, i := range images { + params.Add("image", i) + } + + response, err := conn.DoRequest(nil, http.MethodPost, "/manifests/create", params) + if err != nil { + return "", err + } + return idr.ID, response.Process(&idr) +} + +// Inspect returns a manifest list for a given name. +func Inspect(ctx context.Context, name string) (*manifest.Schema2List, error) { + var list manifest.Schema2List + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/manifests/%s/json", nil, name) + if err != nil { + return nil, err + } + return &list, response.Process(&list) +} + +// Add adds a manifest to a given manifest list. Additional options for the manifest +// can also be specified. The ID of the new manifest list is returned as a string +func Add(ctx context.Context, name string, options image.ManifestAddOpts) (string, error) { + var idr handlers.IDResponse + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + optionsString, err := jsoniter.MarshalToString(options) + if err != nil { + return "", err + } + stringReader := strings.NewReader(optionsString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/manifests/%s/add", nil, name) + if err != nil { + return "", err + } + return idr.ID, response.Process(&idr) +} + +// Remove deletes a manifest entry from a manifest list. Both name and the digest to be +// removed are mandatory inputs. The ID of the new manifest list is returned as a string. +func Remove(ctx context.Context, name, digest string) (string, error) { + var idr handlers.IDResponse + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + params := url.Values{} + params.Set("digest", digest) + response, err := conn.DoRequest(nil, http.MethodDelete, "/manifests/%s", params, name) + if err != nil { + return "", err + } + return idr.ID, response.Process(&idr) +} + +// Push takes a manifest list and pushes to a destination. If the destination is not specified, +// the name will be used instead. If the optional all boolean is specified, all images specified +// in the list will be pushed as well. +func Push(ctx context.Context, name string, destination *string, all *bool) (string, error) { + var ( + idr handlers.IDResponse + ) + dest := name + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + params := url.Values{} + params.Set("image", name) + if destination != nil { + dest = name + } + params.Set("destination", dest) + if all != nil { + params.Set("all", strconv.FormatBool(*all)) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/manifests/%s/push", params, name) + if err != nil { + return "", err + } + return idr.ID, response.Process(&idr) +} diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 1a8c31be1..bb0abebc4 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -5,14 +5,33 @@ import ( "net/http" "net/url" "strconv" + "strings" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" + jsoniter "github.com/json-iterator/go" ) -func CreatePod() error { - // TODO - return bindings.ErrNotImplemented +func CreatePodFromSpec(ctx context.Context, s *specgen.PodSpecGenerator) (*entities.PodCreateReport, error) { + var ( + pcr entities.PodCreateReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + specgenString, err := jsoniter.MarshalToString(s) + if err != nil { + return nil, err + } + stringReader := strings.NewReader(specgenString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/pods/create", nil) + if err != nil { + return nil, err + } + return &pcr, response.Process(&pcr) } // Exists is a lightweight method to determine if a pod exists in local storage @@ -44,10 +63,13 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { // Kill sends a SIGTERM to all the containers in a pod. The optional signal parameter // can be used to override SIGTERM. -func Kill(ctx context.Context, nameOrID string, signal *string) error { +func Kill(ctx context.Context, nameOrID string, signal *string) (*entities.PodKillReport, error) { + var ( + report entities.PodKillReport + ) conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } params := url.Values{} if signal != nil { @@ -55,22 +77,23 @@ func Kill(ctx context.Context, nameOrID string, signal *string) error { } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/kill", params, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + return &report, response.Process(&report) } // Pause pauses all running containers in a given pod. -func Pause(ctx context.Context, nameOrID string) error { +func Pause(ctx context.Context, nameOrID string) (*entities.PodPauseReport, error) { + var report entities.PodPauseReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/pause", nil, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + return &report, response.Process(&report) } // Prune removes all non-running pods in local storage. @@ -88,9 +111,9 @@ func Prune(ctx context.Context) error { // List returns all pods in local storage. The optional filters parameter can // be used to refine which pods should be listed. -func List(ctx context.Context, filters map[string][]string) ([]*libpod.PodInspect, error) { +func List(ctx context.Context, filters map[string][]string) ([]*entities.ListPodsReport, error) { var ( - inspect []*libpod.PodInspect + podsReports []*entities.ListPodsReport ) conn, err := bindings.GetClient(ctx) if err != nil { @@ -106,30 +129,32 @@ func List(ctx context.Context, filters map[string][]string) ([]*libpod.PodInspec } response, err := conn.DoRequest(nil, http.MethodGet, "/pods/json", params) if err != nil { - return inspect, err + return podsReports, err } - return inspect, response.Process(&inspect) + return podsReports, response.Process(&podsReports) } // Restart restarts all containers in a pod. -func Restart(ctx context.Context, nameOrID string) error { +func Restart(ctx context.Context, nameOrID string) (*entities.PodRestartReport, error) { + var report entities.PodRestartReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/restart", nil, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + return &report, response.Process(&report) } // Remove deletes a Pod from from local storage. The optional force parameter denotes // that the Pod can be removed even if in a running state. -func Remove(ctx context.Context, nameOrID string, force *bool) error { +func Remove(ctx context.Context, nameOrID string, force *bool) (*entities.PodRmReport, error) { + var report entities.PodRmReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } params := url.Values{} if force != nil { @@ -137,22 +162,27 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { } response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s", params, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + return &report, response.Process(&report) } // Start starts all containers in a pod. -func Start(ctx context.Context, nameOrID string) error { +func Start(ctx context.Context, nameOrID string) (*entities.PodStartReport, error) { + var report entities.PodStartReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/start", nil, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + if response.StatusCode == http.StatusNotModified { + report.Id = nameOrID + return &report, nil + } + return &report, response.Process(&report) } func Stats() error { @@ -162,10 +192,11 @@ func Stats() error { // Stop stops all containers in a Pod. The optional timeout parameter can be // used to override the timeout before the container is killed. -func Stop(ctx context.Context, nameOrID string, timeout *int) error { +func Stop(ctx context.Context, nameOrID string, timeout *int) (*entities.PodStopReport, error) { + var report entities.PodStopReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } params := url.Values{} if timeout != nil { @@ -173,9 +204,13 @@ func Stop(ctx context.Context, nameOrID string, timeout *int) error { } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/stop", params, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + if response.StatusCode == http.StatusNotModified { + report.Id = nameOrID + return &report, nil + } + return &report, response.Process(&report) } func Top() error { @@ -184,14 +219,15 @@ func Top() error { } // Unpause unpauses all paused containers in a Pod. -func Unpause(ctx context.Context, nameOrID string) error { +func Unpause(ctx context.Context, nameOrID string) (*entities.PodUnpauseReport, error) { + var report entities.PodUnpauseReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return nil, err } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/unpause", nil, nameOrID) if err != nil { - return err + return nil, err } - return response.Process(nil) + return &report, response.Process(&report) } diff --git a/pkg/bindings/system/system.go b/pkg/bindings/system/system.go new file mode 100644 index 000000000..fce8bbb8e --- /dev/null +++ b/pkg/bindings/system/system.go @@ -0,0 +1,61 @@ +package system + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Events allows you to monitor libdpod related events like container creation and +// removal. The events are then passed to the eventChan provided. The optional cancelChan +// can be used to cancel the read of events and close down the HTTP connection. +func Events(ctx context.Context, eventChan chan (handlers.Event), cancelChan chan bool, since, until *string, filters map[string][]string) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + params := url.Values{} + if since != nil { + params.Set("since", *since) + } + if until != nil { + params.Set("until", *until) + } + if filters != nil { + filterString, err := bindings.FiltersToString(filters) + if err != nil { + return errors.Wrap(err, "invalid filters") + } + params.Set("filters", filterString) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/events", params) + if err != nil { + return err + } + if cancelChan != nil { + go func() { + <-cancelChan + err = response.Body.Close() + logrus.Error(errors.Wrap(err, "unable to close event response body")) + }() + } + dec := json.NewDecoder(response.Body) + for { + e := handlers.Event{} + if err := dec.Decode(&e); err != nil { + if err == io.EOF { + break + } + return errors.Wrap(err, "unable to decode event response") + } + eventChan <- e + } + return nil +} diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 38f5014ca..6b8d6788c 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -1,13 +1,18 @@ package test_bindings import ( + "context" "fmt" + "github.com/containers/libpod/libpod/define" "io/ioutil" "os" "os/exec" "path/filepath" "strings" + . "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/specgen" "github.com/onsi/ginkgo" "github.com/onsi/gomega/gexec" "github.com/pkg/errors" @@ -55,6 +60,16 @@ type bindingTest struct { tempDirPath string runRoot string crioRoot string + conn context.Context +} + +func (b *bindingTest) NewConnection() error { + connText, err := NewConnection(context.Background(), b.sock) + if err != nil { + return err + } + b.conn = connText + return nil } func (b *bindingTest) runPodman(command []string) *gexec.Session { @@ -138,7 +153,7 @@ func (b *bindingTest) startAPIService() *gexec.Session { var ( cmd []string ) - cmd = append(cmd, "--log-level=debug", "system", "service", "--timeout=999999", b.sock) + cmd = append(cmd, "--log-level=debug", "--events-backend=file", "system", "service", "--timeout=0", b.sock) return b.runPodman(cmd) } @@ -173,17 +188,27 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { // Run a container within or without a pod // and add or append the alpine image to it -func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) { - cmd := []string{"run", "-dt"} +func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) (string, error) { + s := specgen.NewSpecGenerator(alpine.name) + s.Terminal = false + s.Command = []string{"top"} + if containerName != nil { + s.Name = *containerName + } if insidePod != nil && podName != nil { - pName := *podName - cmd = append(cmd, "--pod", pName) - } else if containerName != nil { - cName := *containerName - cmd = append(cmd, "--name", cName) + s.Pod = *podName } - cmd = append(cmd, alpine.name, "top") - b.runPodman(cmd).Wait(45) + ctr, err := containers.CreateWithSpec(b.conn, s) + if err != nil { + return "", nil + } + err = containers.Start(b.conn, ctr.ID, nil) + if err != nil { + return "", err + } + wait := define.ContainerStateRunning + _, err = containers.Wait(b.conn, ctr.ID, &wait) + return ctr.ID, err } // This method creates a pod with the given pod name. @@ -240,3 +265,7 @@ func createCache() { } b.cleanup() } + +func isStopped(state string) bool { + return state == "exited" || state == "stopped" +} diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index 6756e81c7..9dd9cb707 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -1,13 +1,15 @@ package test_bindings import ( - "context" "net/http" "strconv" + "strings" "time" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -16,12 +18,9 @@ import ( var _ = Describe("Podman containers ", func() { var ( - bt *bindingTest - s *gexec.Session - connText context.Context - err error - falseFlag bool = false - trueFlag bool = true + bt *bindingTest + s *gexec.Session + err error ) BeforeEach(func() { @@ -29,7 +28,7 @@ var _ = Describe("Podman containers ", func() { bt.RestoreImagesFromCache() s = bt.startAPIService() time.Sleep(1 * time.Second) - connText, err = bindings.NewConnection(context.Background(), bt.sock) + err := bt.NewConnection() Expect(err).To(BeNil()) }) @@ -40,7 +39,7 @@ var _ = Describe("Podman containers ", func() { It("podman pause a bogus container", func() { // Pausing bogus container should return 404 - err = containers.Pause(connText, "foobar") + err = containers.Pause(bt.conn, "foobar") Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) @@ -48,7 +47,7 @@ var _ = Describe("Podman containers ", func() { It("podman unpause a bogus container", func() { // Unpausing bogus container should return 404 - err = containers.Unpause(connText, "foobar") + err = containers.Unpause(bt.conn, "foobar") Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) @@ -57,12 +56,13 @@ var _ = Describe("Podman containers ", func() { It("podman pause a running container by name", func() { // Pausing by name should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Pause(connText, name) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Pause(bt.conn, name) Expect(err).To(BeNil()) // Ensure container is paused - data, err := containers.Inspect(connText, name, nil) + data, err := containers.Inspect(bt.conn, name, nil) Expect(err).To(BeNil()) Expect(data.State.Status).To(Equal("paused")) }) @@ -70,54 +70,60 @@ var _ = Describe("Podman containers ", func() { It("podman pause a running container by id", func() { // Pausing by id should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).To(BeNil()) // Ensure container is paused - data, err = containers.Inspect(connText, data.ID, nil) + data, err := containers.Inspect(bt.conn, cid, nil) + Expect(err).To(BeNil()) Expect(data.State.Status).To(Equal("paused")) }) It("podman unpause a running container by name", func() { // Unpausing by name should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Pause(connText, name) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Unpause(connText, name) + err = containers.Pause(bt.conn, name) + Expect(err).To(BeNil()) + err = containers.Unpause(bt.conn, name) Expect(err).To(BeNil()) // Ensure container is unpaused - data, err := containers.Inspect(connText, name, nil) + data, err := containers.Inspect(bt.conn, name, nil) + Expect(err).To(BeNil()) Expect(data.State.Status).To(Equal("running")) }) It("podman unpause a running container by ID", func() { // Unpausing by ID should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - // Pause by name - err := containers.Pause(connText, name) - data, err := containers.Inspect(connText, name, nil) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Unpause(connText, data.ID) + // Pause by name + err = containers.Pause(bt.conn, name) + //paused := "paused" + //_, err = containers.Wait(bt.conn, cid, &paused) + //Expect(err).To(BeNil()) + err = containers.Unpause(bt.conn, name) Expect(err).To(BeNil()) // Ensure container is unpaused - data, err = containers.Inspect(connText, name, nil) + data, err := containers.Inspect(bt.conn, name, nil) + Expect(err).To(BeNil()) Expect(data.State.Status).To(Equal("running")) }) It("podman pause a paused container by name", func() { // Pausing a paused container by name should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Pause(connText, name) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Pause(bt.conn, name) Expect(err).To(BeNil()) - err = containers.Pause(connText, name) + err = containers.Pause(bt.conn, name) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -126,12 +132,11 @@ var _ = Describe("Podman containers ", func() { It("podman pause a paused container by id", func() { // Pausing a paused container by id should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -140,10 +145,11 @@ var _ = Describe("Podman containers ", func() { It("podman pause a stopped container by name", func() { // Pausing a stopped container by name should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Stop(connText, name, nil) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, name) + err = containers.Stop(bt.conn, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(bt.conn, name) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -152,11 +158,11 @@ var _ = Describe("Podman containers ", func() { It("podman pause a stopped container by id", func() { // Pausing a stopped container by id should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) - err = containers.Stop(connText, data.ID, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, cid, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -165,12 +171,11 @@ var _ = Describe("Podman containers ", func() { It("podman remove a paused container by id without force", func() { // Removing a paused container without force should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).To(BeNil()) - err = containers.Remove(connText, data.ID, &falseFlag, &falseFlag) + err = containers.Remove(bt.conn, cid, &bindings.PFalse, &bindings.PFalse) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -187,22 +192,22 @@ var _ = Describe("Podman containers ", func() { // Removing a paused container with force should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).To(BeNil()) - err = containers.Remove(connText, data.ID, &trueFlag, &falseFlag) + err = containers.Remove(bt.conn, cid, &bindings.PTrue, &bindings.PFalse) Expect(err).To(BeNil()) }) It("podman stop a paused container by name", func() { // Stopping a paused container by name should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Pause(connText, name) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Stop(connText, name, nil) + err = containers.Pause(bt.conn, name) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, name, nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -211,12 +216,11 @@ var _ = Describe("Podman containers ", func() { It("podman stop a paused container by id", func() { // Stopping a paused container by id should fail var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Pause(connText, data.ID) + err = containers.Pause(bt.conn, cid) Expect(err).To(BeNil()) - err = containers.Stop(connText, data.ID, nil) + err = containers.Stop(bt.conn, cid, nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -225,29 +229,185 @@ var _ = Describe("Podman containers ", func() { It("podman stop a running container by name", func() { // Stopping a running container by name should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - err := containers.Stop(connText, name, nil) + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, name, nil) Expect(err).To(BeNil()) // Ensure container is stopped - data, err := containers.Inspect(connText, name, nil) + data, err := containers.Inspect(bt.conn, name, nil) Expect(err).To(BeNil()) - Expect(data.State.Status).To(Equal("exited")) + Expect(isStopped(data.State.Status)).To(BeTrue()) }) It("podman stop a running container by ID", func() { // Stopping a running container by ID should work var name = "top" - bt.RunTopContainer(&name, &falseFlag, nil) - data, err := containers.Inspect(connText, name, nil) + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) Expect(err).To(BeNil()) - err = containers.Stop(connText, data.ID, nil) + err = containers.Stop(bt.conn, cid, nil) Expect(err).To(BeNil()) // Ensure container is stopped - data, err = containers.Inspect(connText, name, nil) + data, err := containers.Inspect(bt.conn, name, nil) + Expect(err).To(BeNil()) + Expect(isStopped(data.State.Status)).To(BeTrue()) + }) + + It("podman wait no condition", func() { + var ( + name = "top" + exitCode int32 = -1 + ) + _, err := containers.Wait(bt.conn, "foobar", nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + errChan := make(chan error) + _, err = bt.RunTopContainer(&name, nil, nil) Expect(err).To(BeNil()) - Expect(data.State.Status).To(Equal("exited")) + go func() { + exitCode, err = containers.Wait(bt.conn, name, nil) + errChan <- err + close(errChan) + }() + err = containers.Stop(bt.conn, name, nil) + Expect(err).To(BeNil()) + wait := <-errChan + Expect(wait).To(BeNil()) + Expect(exitCode).To(BeNumerically("==", 143)) }) + It("podman wait to pause|unpause condition", func() { + var ( + name = "top" + exitCode int32 = -1 + pause = define.ContainerStatePaused + running = define.ContainerStateRunning + ) + errChan := make(chan error) + _, err := bt.RunTopContainer(&name, nil, nil) + Expect(err).To(BeNil()) + go func() { + exitCode, err = containers.Wait(bt.conn, name, &pause) + errChan <- err + close(errChan) + }() + err = containers.Pause(bt.conn, name) + Expect(err).To(BeNil()) + wait := <-errChan + Expect(wait).To(BeNil()) + Expect(exitCode).To(BeNumerically("==", -1)) + + errChan = make(chan error) + go func() { + _, waitErr := containers.Wait(bt.conn, name, &running) + errChan <- waitErr + close(errChan) + }() + err = containers.Unpause(bt.conn, name) + Expect(err).To(BeNil()) + unPausewait := <-errChan + Expect(unPausewait).To(BeNil()) + Expect(exitCode).To(BeNumerically("==", -1)) + }) + + It("run healthcheck", func() { + bt.runPodman([]string{"run", "-d", "--name", "hc", "--health-interval", "disable", "--health-retries", "2", "--health-cmd", "ls / || exit 1", alpine.name, "top"}) + + // bogus name should result in 404 + _, err := containers.RunHealthCheck(bt.conn, "foobar") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // a container that has no healthcheck should be a 409 + var name = "top" + bt.RunTopContainer(&name, &bindings.PFalse, nil) + _, err = containers.RunHealthCheck(bt.conn, name) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusConflict)) + + // TODO for the life of me, i cannot get this to work. maybe another set + // of eyes will + // successful healthcheck + //status := "healthy" + //for i:=0; i < 10; i++ { + // result, err := containers.RunHealthCheck(connText, "hc") + // Expect(err).To(BeNil()) + // if result.Status != "healthy" { + // fmt.Println("Healthcheck container still starting, retrying in 1 second") + // time.Sleep(1 * time.Second) + // continue + // } + // status = result.Status + // break + //} + //Expect(status).To(Equal("healthy")) + + // TODO enable this when wait is working + // healthcheck on a stopped container should be a 409 + //err = containers.Stop(connText, "hc", nil) + //Expect(err).To(BeNil()) + //_, err = containers.Wait(connText, "hc") + //Expect(err).To(BeNil()) + //_, err = containers.RunHealthCheck(connText, "hc") + //code, _ = bindings.CheckResponseCode(err) + //Expect(code).To(BeNumerically("==", http.StatusConflict)) + }) + + It("logging", func() { + stdoutChan := make(chan string, 10) + s := specgen.NewSpecGenerator(alpine.name) + s.Terminal = true + s.Command = []string{"date", "-R"} + r, err := containers.CreateWithSpec(bt.conn, s) + Expect(err).To(BeNil()) + err = containers.Start(bt.conn, r.ID, nil) + Expect(err).To(BeNil()) + + _, err = containers.Wait(bt.conn, r.ID, nil) + Expect(err).To(BeNil()) + + opts := containers.LogOptions{Stdout: &bindings.PTrue, Follow: &bindings.PTrue} + go func() { + containers.Logs(bt.conn, r.ID, opts, stdoutChan, nil) + }() + o := <-stdoutChan + o = strings.ReplaceAll(o, "\r", "") + _, err = time.Parse(time.RFC1123Z, o) + Expect(err).To(BeNil()) + }) + + It("podman top", func() { + var name = "top" + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + // By name + output, err := containers.Top(bt.conn, name, nil) + Expect(err).To(BeNil()) + + // By id + output, err = containers.Top(bt.conn, cid, nil) + Expect(err).To(BeNil()) + + // With descriptors + output, err = containers.Top(bt.conn, cid, []string{"user,pid,hpid"}) + Expect(err).To(BeNil()) + header := strings.Split(output[0], "\t") + for _, d := range []string{"USER", "PID", "HPID"} { + Expect(d).To(BeElementOf(header)) + } + + // With bogus ID + _, err = containers.Top(bt.conn, "IdoNotExist", nil) + Expect(err).ToNot(BeNil()) + + // With bogus descriptors + _, err = containers.Top(bt.conn, cid, []string{"Me,Neither"}) + Expect(err).To(BeNil()) + }) }) diff --git a/pkg/bindings/test/create_test.go b/pkg/bindings/test/create_test.go new file mode 100644 index 000000000..f83a9b14d --- /dev/null +++ b/pkg/bindings/test/create_test.go @@ -0,0 +1,50 @@ +package test_bindings + +import ( + "time" + + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/specgen" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Create containers ", func() { + var ( + bt *bindingTest + s *gexec.Session + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + err := bt.NewConnection() + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("create a container running top", func() { + s := specgen.NewSpecGenerator(alpine.name) + s.Command = []string{"top"} + s.Terminal = true + s.Name = "top" + ctr, err := containers.CreateWithSpec(bt.conn, s) + Expect(err).To(BeNil()) + data, err := containers.Inspect(bt.conn, ctr.ID, nil) + Expect(err).To(BeNil()) + Expect(data.Name).To(Equal("top")) + err = containers.Start(bt.conn, ctr.ID, nil) + Expect(err).To(BeNil()) + data, err = containers.Inspect(bt.conn, ctr.ID, nil) + Expect(err).To(BeNil()) + Expect(data.State.Status).To(Equal("running")) + }) + +}) diff --git a/pkg/bindings/test/exec_test.go b/pkg/bindings/test/exec_test.go new file mode 100644 index 000000000..1ef2197b6 --- /dev/null +++ b/pkg/bindings/test/exec_test.go @@ -0,0 +1,77 @@ +package test_bindings + +import ( + "time" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman containers exec", func() { + var ( + bt *bindingTest + s *gexec.Session + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + err := bt.NewConnection() + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("Podman exec create makes an exec session", func() { + name := "testCtr" + cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + execConfig := new(handlers.ExecCreateConfig) + execConfig.Cmd = []string{"echo", "hello world"} + + sessionID, err := containers.ExecCreate(bt.conn, name, execConfig) + Expect(err).To(BeNil()) + Expect(sessionID).To(Not(Equal(""))) + + inspectOut, err := containers.ExecInspect(bt.conn, sessionID) + Expect(err).To(BeNil()) + Expect(inspectOut.ContainerID).To(Equal(cid)) + Expect(inspectOut.ProcessConfig.Entrypoint).To(Equal("echo")) + Expect(len(inspectOut.ProcessConfig.Arguments)).To(Equal(1)) + Expect(inspectOut.ProcessConfig.Arguments[0]).To(Equal("hello world")) + }) + + It("Podman exec create with bad command fails", func() { + name := "testCtr" + _, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + execConfig := new(handlers.ExecCreateConfig) + + _, err = containers.ExecCreate(bt.conn, name, execConfig) + Expect(err).To(Not(BeNil())) + }) + + It("Podman exec create with invalid container fails", func() { + execConfig := new(handlers.ExecCreateConfig) + execConfig.Cmd = []string{"echo", "hello world"} + + _, err := containers.ExecCreate(bt.conn, "doesnotexist", execConfig) + Expect(err).To(Not(BeNil())) + }) + + It("Podman exec inspect on invalid session fails", func() { + _, err := containers.ExecInspect(bt.conn, "0000000000000000000000000000000000000000000000000000000000000000") + Expect(err).To(Not(BeNil())) + }) +}) diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 8eef28502..13b6086c3 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -1,7 +1,6 @@ package test_bindings import ( - "context" "net/http" "os" "path/filepath" @@ -17,68 +16,66 @@ import ( var _ = Describe("Podman images", func() { var ( - //tempdir string - //err error - //podmanTest *PodmanTestIntegration - bt *bindingTest - s *gexec.Session - connText context.Context - err error - falseFlag bool = false - trueFlag bool = true + // tempdir string + // err error + // podmanTest *PodmanTestIntegration + bt *bindingTest + s *gexec.Session + err error ) BeforeEach(func() { - //tempdir, err = CreateTempDirInTempDir() - //if err != nil { + // tempdir, err = CreateTempDirInTempDir() + // if err != nil { // os.Exit(1) - //} - //podmanTest = PodmanTestCreate(tempdir) - //podmanTest.Setup() - //podmanTest.SeedImages() + // } + // podmanTest = PodmanTestCreate(tempdir) + // podmanTest.Setup() + // podmanTest.SeedImages() bt = newBindingTest() bt.RestoreImagesFromCache() s = bt.startAPIService() time.Sleep(1 * time.Second) - connText, err = bindings.NewConnection(context.Background(), bt.sock) + err := bt.NewConnection() Expect(err).To(BeNil()) }) AfterEach(func() { - //podmanTest.Cleanup() - //f := CurrentGinkgoTestDescription() - //processTestResult(f) + // podmanTest.Cleanup() + // f := CurrentGinkgoTestDescription() + // processTestResult(f) s.Kill() bt.cleanup() }) + It("inspect image", func() { // Inspect invalid image be 404 - _, err = images.GetImage(connText, "foobar5000", nil) + _, err = images.GetImage(bt.conn, "foobar5000", nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Inspect by short name - data, err := images.GetImage(connText, alpine.shortName, nil) + data, err := images.GetImage(bt.conn, alpine.shortName, nil) Expect(err).To(BeNil()) // Inspect with full ID - _, err = images.GetImage(connText, data.ID, nil) + _, err = images.GetImage(bt.conn, data.ID, nil) Expect(err).To(BeNil()) // Inspect with partial ID - _, err = images.GetImage(connText, data.ID[0:12], nil) + _, err = images.GetImage(bt.conn, data.ID[0:12], nil) Expect(err).To(BeNil()) // Inspect by long name - _, err = images.GetImage(connText, alpine.name, nil) + _, err = images.GetImage(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) // TODO it looks like the images API alwaays returns size regardless // of bool or not. What should we do ? - //Expect(data.Size).To(BeZero()) + // Expect(data.Size).To(BeZero()) // Enabling the size parameter should result in size being populated - data, err = images.GetImage(connText, alpine.name, &trueFlag) + data, err = images.GetImage(bt.conn, alpine.name, &bindings.PTrue) Expect(err).To(BeNil()) Expect(data.Size).To(BeNumerically(">", 0)) }) @@ -86,49 +83,50 @@ var _ = Describe("Podman images", func() { // Test to validate the remove image api It("remove image", func() { // Remove invalid image should be a 404 - _, err = images.Remove(connText, "foobar5000", &falseFlag) + _, err = images.Remove(bt.conn, "foobar5000", &bindings.PFalse) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Remove an image by name, validate image is removed and error is nil - inspectData, err := images.GetImage(connText, busybox.shortName, nil) + inspectData, err := images.GetImage(bt.conn, busybox.shortName, nil) Expect(err).To(BeNil()) - response, err := images.Remove(connText, busybox.shortName, nil) + response, err := images.Remove(bt.conn, busybox.shortName, nil) Expect(err).To(BeNil()) Expect(inspectData.ID).To(Equal(response[0]["Deleted"])) - inspectData, err = images.GetImage(connText, busybox.shortName, nil) + inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Start a container with alpine image var top string = "top" - bt.RunTopContainer(&top, &falseFlag, nil) + _, err = bt.RunTopContainer(&top, &bindings.PFalse, nil) + Expect(err).To(BeNil()) // we should now have a container called "top" running - containerResponse, err := containers.Inspect(connText, "top", &falseFlag) + containerResponse, err := containers.Inspect(bt.conn, "top", &bindings.PFalse) Expect(err).To(BeNil()) Expect(containerResponse.Name).To(Equal("top")) // try to remove the image "alpine". This should fail since we are not force // deleting hence image cannot be deleted until the container is deleted. - response, err = images.Remove(connText, alpine.shortName, &falseFlag) + response, err = images.Remove(bt.conn, alpine.shortName, &bindings.PFalse) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) // Removing the image "alpine" where force = true - response, err = images.Remove(connText, alpine.shortName, &trueFlag) + response, err = images.Remove(bt.conn, alpine.shortName, &bindings.PTrue) Expect(err).To(BeNil()) // Checking if both the images are gone as well as the container is deleted - inspectData, err = images.GetImage(connText, busybox.shortName, nil) + inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - inspectData, err = images.GetImage(connText, alpine.shortName, nil) + inspectData, err = images.GetImage(bt.conn, alpine.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - _, err = containers.Inspect(connText, "top", &falseFlag) + _, err = containers.Inspect(bt.conn, "top", &bindings.PFalse) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) }) @@ -136,17 +134,17 @@ var _ = Describe("Podman images", func() { // Tests to validate the image tag command. It("tag image", func() { // Validates if invalid image name is given a bad response is encountered. - err = images.Tag(connText, "dummy", "demo", alpine.shortName) + err = images.Tag(bt.conn, "dummy", "demo", alpine.shortName) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - // Validates if the image is tagged sucessfully. - err = images.Tag(connText, alpine.shortName, "demo", alpine.shortName) + // Validates if the image is tagged successfully. + err = images.Tag(bt.conn, alpine.shortName, "demo", alpine.shortName) Expect(err).To(BeNil()) - //Validates if name updates when the image is retagged. - _, err := images.GetImage(connText, "alpine:demo", nil) + // Validates if name updates when the image is retagged. + _, err := images.GetImage(bt.conn, "alpine:demo", nil) Expect(err).To(BeNil()) }) @@ -154,7 +152,7 @@ var _ = Describe("Podman images", func() { // Test to validate the List images command. It("List image", func() { // Array to hold the list of images returned - imageSummary, err := images.List(connText, nil, nil) + imageSummary, err := images.List(bt.conn, nil, nil) // There Should be no errors in the response. Expect(err).To(BeNil()) // Since in the begin context two images are created the @@ -164,11 +162,11 @@ var _ = Describe("Podman images", func() { // Adding one more image. There Should be no errors in the response. // And the count should be three now. bt.Pull("busybox:glibc") - imageSummary, err = images.List(connText, nil, nil) + imageSummary, err = images.List(bt.conn, nil, nil) Expect(err).To(BeNil()) Expect(len(imageSummary)).To(Equal(3)) - //Validate the image names. + // Validate the image names. var names []string for _, i := range imageSummary { names = append(names, i.RepoTags...) @@ -179,13 +177,13 @@ var _ = Describe("Podman images", func() { // List images with a filter filters := make(map[string][]string) filters["reference"] = []string{alpine.name} - filteredImages, err := images.List(connText, &falseFlag, filters) + filteredImages, err := images.List(bt.conn, &bindings.PFalse, filters) Expect(err).To(BeNil()) Expect(len(filteredImages)).To(BeNumerically("==", 1)) // List images with a bad filter filters["name"] = []string{alpine.name} - _, err = images.List(connText, &falseFlag, filters) + _, err = images.List(bt.conn, &bindings.PFalse, filters) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -193,64 +191,64 @@ var _ = Describe("Podman images", func() { It("Image Exists", func() { // exists on bogus image should be false, with no error - exists, err := images.Exists(connText, "foobar") + exists, err := images.Exists(bt.conn, "foobar") Expect(err).To(BeNil()) Expect(exists).To(BeFalse()) // exists with shortname should be true - exists, err = images.Exists(connText, alpine.shortName) + exists, err = images.Exists(bt.conn, alpine.shortName) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) // exists with fqname should be true - exists, err = images.Exists(connText, alpine.name) + exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) }) It("Load|Import Image", func() { // load an image - _, err := images.Remove(connText, alpine.name, nil) + _, err := images.Remove(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) - exists, err := images.Exists(connText, alpine.name) + exists, err := images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeFalse()) f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) defer f.Close() Expect(err).To(BeNil()) - names, err := images.Load(connText, f, nil) + names, err := images.Load(bt.conn, f, nil) Expect(err).To(BeNil()) Expect(names).To(Equal(alpine.name)) - exists, err = images.Exists(connText, alpine.name) + exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) // load with a repo name f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) Expect(err).To(BeNil()) - _, err = images.Remove(connText, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) - exists, err = images.Exists(connText, alpine.name) + exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeFalse()) newName := "quay.io/newname:fizzle" - names, err = images.Load(connText, f, &newName) + names, err = images.Load(bt.conn, f, &newName) Expect(err).To(BeNil()) Expect(names).To(Equal(alpine.name)) - exists, err = images.Exists(connText, newName) + exists, err = images.Exists(bt.conn, newName) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) // load with a bad repo name should trigger a 500 f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) Expect(err).To(BeNil()) - _, err = images.Remove(connText, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) - exists, err = images.Exists(connText, alpine.name) + exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeFalse()) badName := "quay.io/newName:fizzle" - _, err = images.Load(connText, f, &badName) + _, err = images.Load(bt.conn, f, &badName) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -262,7 +260,7 @@ var _ = Describe("Podman images", func() { w, err := os.Create(filepath.Join(bt.tempDirPath, alpine.tarballName)) defer w.Close() Expect(err).To(BeNil()) - err = images.Export(connText, alpine.name, w, nil, nil) + err = images.Export(bt.conn, alpine.name, w, nil, nil) Expect(err).To(BeNil()) _, err = os.Stat(exportPath) Expect(err).To(BeNil()) @@ -272,9 +270,9 @@ var _ = Describe("Podman images", func() { It("Import Image", func() { // load an image - _, err = images.Remove(connText, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) - exists, err := images.Exists(connText, alpine.name) + exists, err := images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeFalse()) f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) @@ -282,27 +280,28 @@ var _ = Describe("Podman images", func() { Expect(err).To(BeNil()) changes := []string{"CMD /bin/foobar"} testMessage := "test_import" - _, err = images.Import(connText, changes, &testMessage, &alpine.name, nil, f) + _, err = images.Import(bt.conn, changes, &testMessage, &alpine.name, nil, f) Expect(err).To(BeNil()) - exists, err = images.Exists(connText, alpine.name) + exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) - data, err := images.GetImage(connText, alpine.name, nil) + data, err := images.GetImage(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) Expect(data.Comment).To(Equal(testMessage)) }) + It("History Image", func() { // a bogus name should return a 404 - _, err := images.History(connText, "foobar") + _, err := images.History(bt.conn, "foobar") Expect(err).To(Not(BeNil())) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) var foundID bool - data, err := images.GetImage(connText, alpine.name, nil) + data, err := images.GetImage(bt.conn, alpine.name, nil) Expect(err).To(BeNil()) - history, err := images.History(connText, alpine.name) + history, err := images.History(bt.conn, alpine.name) Expect(err).To(BeNil()) for _, i := range history { if i.ID == data.ID { @@ -314,7 +313,7 @@ var _ = Describe("Podman images", func() { }) It("Search for an image", func() { - imgs, err := images.Search(connText, "alpine", nil, nil) + imgs, err := images.Search(bt.conn, "alpine", nil, nil) Expect(err).To(BeNil()) Expect(len(imgs)).To(BeNumerically(">", 1)) var foundAlpine bool @@ -328,22 +327,30 @@ var _ = Describe("Podman images", func() { // Search for alpine with a limit of 10 ten := 10 - imgs, err = images.Search(connText, "docker.io/alpine", &ten, nil) + imgs, err = images.Search(bt.conn, "docker.io/alpine", &ten, nil) Expect(err).To(BeNil()) Expect(len(imgs)).To(BeNumerically("<=", 10)) // Search for alpine with stars greater than 100 filters := make(map[string][]string) filters["stars"] = []string{"100"} - imgs, err = images.Search(connText, "docker.io/alpine", nil, filters) + imgs, err = images.Search(bt.conn, "docker.io/alpine", nil, filters) Expect(err).To(BeNil()) for _, i := range imgs { Expect(i.Stars).To(BeNumerically(">=", 100)) } // Search with a fqdn - imgs, err = images.Search(connText, "quay.io/libpod/alpine_nginx", nil, nil) + imgs, err = images.Search(bt.conn, "quay.io/libpod/alpine_nginx", nil, nil) Expect(len(imgs)).To(BeNumerically(">=", 1)) }) + It("Prune images", func() { + trueBoxed := true + results, err := images.Prune(bt.conn, &trueBoxed, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(len(results)).To(BeNumerically(">", 0)) + Expect(results).To(ContainElement("docker.io/library/alpine:latest")) + }) + }) diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go new file mode 100644 index 000000000..23c3d8194 --- /dev/null +++ b/pkg/bindings/test/manifests_test.go @@ -0,0 +1,124 @@ +package test_bindings + +import ( + "net/http" + "time" + + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/images" + "github.com/containers/libpod/pkg/bindings/manifests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman containers ", func() { + var ( + bt *bindingTest + s *gexec.Session + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + err := bt.NewConnection() + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("create manifest", func() { + // create manifest list without images + id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) + Expect(err).To(BeNil()) + list, err := manifests.Inspect(bt.conn, id) + Expect(err).To(BeNil()) + Expect(len(list.Manifests)).To(BeZero()) + + // creating a duplicate should fail as a 500 + _, err = manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + + _, err = images.Remove(bt.conn, id, nil) + Expect(err).To(BeNil()) + + // create manifest list with images + id, err = manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{alpine.name}, nil) + Expect(err).To(BeNil()) + list, err = manifests.Inspect(bt.conn, id) + Expect(err).To(BeNil()) + Expect(len(list.Manifests)).To(BeNumerically("==", 1)) + }) + + It("inspect bogus manifest", func() { + _, err := manifests.Inspect(bt.conn, "larry") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + + It("add manifest", func() { + // add to bogus should 404 + _, err := manifests.Add(bt.conn, "foobar", image.ManifestAddOpts{}) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{}, nil) + Expect(err).To(BeNil()) + opts := image.ManifestAddOpts{Images: []string{alpine.name}} + _, err = manifests.Add(bt.conn, id, opts) + Expect(err).To(BeNil()) + list, err := manifests.Inspect(bt.conn, id) + Expect(err).To(BeNil()) + Expect(len(list.Manifests)).To(BeNumerically("==", 1)) + + // add bogus name to existing list should fail + opts.Images = []string{"larry"} + _, err = manifests.Add(bt.conn, id, opts) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("remove manifest", func() { + // removal on bogus manifest list should be 404 + _, err := manifests.Remove(bt.conn, "larry", "1234") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + id, err := manifests.Create(bt.conn, []string{"quay.io/libpod/foobar:latest"}, []string{alpine.name}, nil) + Expect(err).To(BeNil()) + data, err := manifests.Inspect(bt.conn, id) + Expect(err).To(BeNil()) + Expect(len(data.Manifests)).To(BeNumerically("==", 1)) + + // removal on a good manifest list with a bad digest should be 400 + _, err = manifests.Remove(bt.conn, id, "!234") + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusBadRequest)) + + digest := data.Manifests[0].Digest.String() + _, err = manifests.Remove(bt.conn, id, digest) + Expect(err).To(BeNil()) + + // removal on good manifest with good digest should work + data, err = manifests.Inspect(bt.conn, id) + Expect(err).To(BeNil()) + Expect(len(data.Manifests)).To(BeZero()) + }) + + It("push manifest", func() { + Skip("TODO") + }) +}) diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go index 4bea2f8d7..0f786e341 100644 --- a/pkg/bindings/test/pods_test.go +++ b/pkg/bindings/test/pods_test.go @@ -1,26 +1,24 @@ package test_bindings import ( - "context" "net/http" "time" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/pods" + "github.com/containers/libpod/pkg/specgen" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" ) -var _ = Describe("Podman images", func() { +var _ = Describe("Podman pods", func() { var ( - bt *bindingTest - s *gexec.Session - connText context.Context - newpod string - err error - trueFlag bool = true + bt *bindingTest + s *gexec.Session + newpod string + err error ) BeforeEach(func() { @@ -30,7 +28,7 @@ var _ = Describe("Podman images", func() { bt.Podcreate(&newpod) s = bt.startAPIService() time.Sleep(1 * time.Second) - connText, err = bindings.NewConnection(context.Background(), bt.sock) + err := bt.NewConnection() Expect(err).To(BeNil()) }) @@ -41,13 +39,13 @@ var _ = Describe("Podman images", func() { It("inspect pod", func() { //Inspect an invalid pod name - _, err := pods.Inspect(connText, "dummyname") + _, err := pods.Inspect(bt.conn, "dummyname") Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) //Inspect an valid pod name - response, err := pods.Inspect(connText, newpod) + response, err := pods.Inspect(bt.conn, newpod) Expect(err).To(BeNil()) Expect(response.Config.Name).To(Equal(newpod)) }) @@ -55,12 +53,13 @@ var _ = Describe("Podman images", func() { // Test validates the list all api returns It("list pod", func() { //List all the pods in the current instance - podSummary, err := pods.List(connText, nil) + podSummary, err := pods.List(bt.conn, nil) Expect(err).To(BeNil()) Expect(len(podSummary)).To(Equal(1)) // Adding an alpine container to the existing pod - bt.RunTopContainer(nil, &trueFlag, &newpod) - podSummary, err = pods.List(connText, nil) + _, err = bt.RunTopContainer(nil, &bindings.PTrue, &newpod) + Expect(err).To(BeNil()) + podSummary, err = pods.List(bt.conn, nil) // Verify no errors. Expect(err).To(BeNil()) // Verify number of containers in the pod. @@ -69,33 +68,85 @@ var _ = Describe("Podman images", func() { // Add multiple pods and verify them by name and size. var newpod2 string = "newpod2" bt.Podcreate(&newpod2) - podSummary, err = pods.List(connText, nil) + podSummary, err = pods.List(bt.conn, nil) Expect(len(podSummary)).To(Equal(2)) var names []string for _, i := range podSummary { - names = append(names, i.Config.Name) + names = append(names, i.Name) } Expect(StringInSlice(newpod, names)).To(BeTrue()) Expect(StringInSlice("newpod2", names)).To(BeTrue()) + }) + + // The test validates the list pod endpoint with passing filters as the params. + It("List pods with filters", func() { + newpod2 := "newpod2" + bt.Podcreate(&newpod2) + _, err = bt.RunTopContainer(nil, &bindings.PTrue, &newpod) + Expect(err).To(BeNil()) + + // Expected err with invalid filter params + filters := make(map[string][]string) + filters["dummy"] = []string{"dummy"} + filteredPods, err := pods.List(bt.conn, filters) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + + // Expected empty response with invalid filters + filters = make(map[string][]string) + filters["name"] = []string{"dummy"} + filteredPods, err = pods.List(bt.conn, filters) + Expect(err).To(BeNil()) + Expect(len(filteredPods)).To(BeNumerically("==", 0)) + + // Validate list pod with name filter + filters = make(map[string][]string) + filters["name"] = []string{newpod2} + filteredPods, err = pods.List(bt.conn, filters) + Expect(err).To(BeNil()) + Expect(len(filteredPods)).To(BeNumerically("==", 1)) + var names []string + for _, i := range filteredPods { + names = append(names, i.Name) + } + Expect(StringInSlice("newpod2", names)).To(BeTrue()) - // TODO not working Because: code to list based on filter - // "not yet implemented", - // Validate list pod with filters - //filters := make(map[string][]string) - //filters["name"] = []string{newpod} - //filteredPods, err := pods.List(connText, filters) - //Expect(err).To(BeNil()) - //Expect(len(filteredPods)).To(BeNumerically("==", 1)) + // Validate list pod with id filter + filters = make(map[string][]string) + response, err := pods.Inspect(bt.conn, newpod) + Expect(err).To(BeNil()) + id := response.Config.ID + filters["id"] = []string{id} + filteredPods, err = pods.List(bt.conn, filters) + Expect(err).To(BeNil()) + Expect(len(filteredPods)).To(BeNumerically("==", 1)) + names = names[:0] + for _, i := range filteredPods { + names = append(names, i.Name) + } + Expect(StringInSlice("newpod", names)).To(BeTrue()) + + // Using multiple filters + filters["name"] = []string{newpod} + filteredPods, err = pods.List(bt.conn, filters) + Expect(err).To(BeNil()) + Expect(len(filteredPods)).To(BeNumerically("==", 1)) + names = names[:0] + for _, i := range filteredPods { + names = append(names, i.Name) + } + Expect(StringInSlice("newpod", names)).To(BeTrue()) }) // The test validates if the exists responds It("exists pod", func() { - response, err := pods.Exists(connText, "dummyName") + response, err := pods.Exists(bt.conn, "dummyName") Expect(err).To(BeNil()) Expect(response).To(BeFalse()) // Should exit with no error and response should be true - response, err = pods.Exists(connText, "newpod") + response, err = pods.Exists(bt.conn, "newpod") Expect(err).To(BeNil()) Expect(response).To(BeTrue()) }) @@ -103,32 +154,37 @@ var _ = Describe("Podman images", func() { // This test validates if All running containers within // each specified pod are paused and unpaused It("pause upause pod", func() { + // TODO fix this + Skip("Pod behavior is jacked right now.") // Pause invalid container - err := pods.Pause(connText, "dummyName") + _, err := pods.Pause(bt.conn, "dummyName") Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Adding an alpine container to the existing pod - bt.RunTopContainer(nil, &trueFlag, &newpod) - response, err := pods.Inspect(connText, newpod) + _, err = bt.RunTopContainer(nil, &bindings.PTrue, &newpod) Expect(err).To(BeNil()) // Binding needs to be modified to inspect the pod state. - // Since we dont have a pod state we inspect the states of the containers within the pod. + // Since we don't have a pod state we inspect the states of the containers within the pod. // Pause a valid container - err = pods.Pause(connText, newpod) + _, err = pods.Pause(bt.conn, newpod) Expect(err).To(BeNil()) - response, err = pods.Inspect(connText, newpod) + response, err := pods.Inspect(bt.conn, newpod) + Expect(err).To(BeNil()) + Expect(response.State.Status).To(Equal(define.PodStatePaused)) for _, i := range response.Containers { Expect(define.StringToContainerStatus(i.State)). To(Equal(define.ContainerStatePaused)) } // Unpause a valid container - err = pods.Unpause(connText, newpod) + _, err = pods.Unpause(bt.conn, newpod) + Expect(err).To(BeNil()) + response, err = pods.Inspect(bt.conn, newpod) Expect(err).To(BeNil()) - response, err = pods.Inspect(connText, newpod) + Expect(response.State.Status).To(Equal(define.PodStateRunning)) for _, i := range response.Containers { Expect(define.StringToContainerStatus(i.State)). To(Equal(define.ContainerStateRunning)) @@ -137,60 +193,130 @@ var _ = Describe("Podman images", func() { It("start stop restart pod", func() { // Start an invalid pod - err = pods.Start(connText, "dummyName") + _, err = pods.Start(bt.conn, "dummyName") Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Stop an invalid pod - err = pods.Stop(connText, "dummyName", nil) + _, err = pods.Stop(bt.conn, "dummyName", nil) Expect(err).ToNot(BeNil()) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Restart an invalid pod - err = pods.Restart(connText, "dummyName") + _, err = pods.Restart(bt.conn, "dummyName") Expect(err).ToNot(BeNil()) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Start a valid pod and inspect status of each container - err = pods.Start(connText, newpod) + _, err = pods.Start(bt.conn, newpod) Expect(err).To(BeNil()) - response, err := pods.Inspect(connText, newpod) + response, err := pods.Inspect(bt.conn, newpod) + Expect(err).To(BeNil()) + Expect(response.State.Status).To(Equal(define.PodStateRunning)) for _, i := range response.Containers { Expect(define.StringToContainerStatus(i.State)). To(Equal(define.ContainerStateRunning)) } // Start an already running pod - err = pods.Start(connText, newpod) + _, err = pods.Start(bt.conn, newpod) Expect(err).To(BeNil()) // Stop the running pods - err = pods.Stop(connText, newpod, nil) + _, err = pods.Stop(bt.conn, newpod, nil) Expect(err).To(BeNil()) - response, _ = pods.Inspect(connText, newpod) + response, _ = pods.Inspect(bt.conn, newpod) + Expect(response.State.Status).To(Equal(define.PodStateExited)) for _, i := range response.Containers { Expect(define.StringToContainerStatus(i.State)). To(Equal(define.ContainerStateStopped)) } // Stop an already stopped pod - err = pods.Stop(connText, newpod, nil) + _, err = pods.Stop(bt.conn, newpod, nil) Expect(err).To(BeNil()) - err = pods.Restart(connText, newpod) + _, err = pods.Restart(bt.conn, newpod) Expect(err).To(BeNil()) - response, _ = pods.Inspect(connText, newpod) + response, _ = pods.Inspect(bt.conn, newpod) + Expect(response.State.Status).To(Equal(define.PodStateRunning)) for _, i := range response.Containers { Expect(define.StringToContainerStatus(i.State)). To(Equal(define.ContainerStateRunning)) } }) - // Remove all stopped pods and their container to be implemented. + // Test to validate all the pods in the stopped/exited state are pruned successfully. It("prune pod", func() { + // Add a new pod + var newpod2 string = "newpod2" + bt.Podcreate(&newpod2) + // No pods pruned since no pod in exited state + err = pods.Prune(bt.conn) + Expect(err).To(BeNil()) + podSummary, err := pods.List(bt.conn, nil) + Expect(err).To(BeNil()) + Expect(len(podSummary)).To(Equal(2)) + + // Prune only one pod which is in exited state. + // Start then stop a pod. + // pod moves to exited state one pod should be pruned now. + _, err = pods.Start(bt.conn, newpod) + Expect(err).To(BeNil()) + _, err = pods.Stop(bt.conn, newpod, nil) + Expect(err).To(BeNil()) + response, err := pods.Inspect(bt.conn, newpod) + Expect(err).To(BeNil()) + Expect(response.State.Status).To(Equal(define.PodStateExited)) + err = pods.Prune(bt.conn) + Expect(err).To(BeNil()) + podSummary, err = pods.List(bt.conn, nil) + Expect(err).To(BeNil()) + Expect(len(podSummary)).To(Equal(1)) + + // Test prune all pods in exited state. + bt.Podcreate(&newpod) + _, err = pods.Start(bt.conn, newpod) + Expect(err).To(BeNil()) + _, err = pods.Start(bt.conn, newpod2) + Expect(err).To(BeNil()) + _, err = pods.Stop(bt.conn, newpod, nil) + Expect(err).To(BeNil()) + response, err = pods.Inspect(bt.conn, newpod) + Expect(err).To(BeNil()) + Expect(response.State.Status).To(Equal(define.PodStateExited)) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateStopped)) + } + _, err = pods.Stop(bt.conn, newpod2, nil) + Expect(err).To(BeNil()) + response, err = pods.Inspect(bt.conn, newpod2) + Expect(err).To(BeNil()) + Expect(response.State.Status).To(Equal(define.PodStateExited)) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateStopped)) + } + err = pods.Prune(bt.conn) + Expect(err).To(BeNil()) + podSummary, err = pods.List(bt.conn, nil) + Expect(err).To(BeNil()) + Expect(len(podSummary)).To(Equal(0)) + }) + + It("simple create pod", func() { + ps := specgen.PodSpecGenerator{} + ps.Name = "foobar" + _, err := pods.CreatePodFromSpec(bt.conn, &ps) + Expect(err).To(BeNil()) + + exists, err := pods.Exists(bt.conn, "foobar") + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) }) }) diff --git a/pkg/bindings/test/system_test.go b/pkg/bindings/test/system_test.go new file mode 100644 index 000000000..3abc26b34 --- /dev/null +++ b/pkg/bindings/test/system_test.go @@ -0,0 +1,51 @@ +package test_bindings + +import ( + "time" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings/system" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman system", func() { + var ( + bt *bindingTest + s *gexec.Session + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + err := bt.NewConnection() + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("podman events", func() { + eChan := make(chan handlers.Event, 1) + var messages []handlers.Event + cancelChan := make(chan bool, 1) + go func() { + for e := range eChan { + messages = append(messages, e) + } + }() + go func() { + system.Events(bt.conn, eChan, cancelChan, nil, nil, nil) + }() + + _, err := bt.RunTopContainer(nil, nil, nil) + Expect(err).To(BeNil()) + cancelChan <- true + Expect(len(messages)).To(BeNumerically("==", 3)) + }) +}) diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go new file mode 100644 index 000000000..59fe48f22 --- /dev/null +++ b/pkg/bindings/test/volumes_test.go @@ -0,0 +1,173 @@ +package test_bindings + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/bindings/volumes" + "github.com/containers/libpod/pkg/domain/entities" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman volumes", func() { + var ( + //tempdir string + //err error + //podmanTest *PodmanTestIntegration + bt *bindingTest + s *gexec.Session + connText context.Context + err error + ) + + BeforeEach(func() { + //tempdir, err = CreateTempDirInTempDir() + //if err != nil { + // os.Exit(1) + //} + //podmanTest = PodmanTestCreate(tempdir) + //podmanTest.Setup() + //podmanTest.SeedImages() + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(context.Background(), bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + //podmanTest.Cleanup() + //f := CurrentGinkgoTestDescription() + //processTestResult(f) + s.Kill() + bt.cleanup() + }) + + It("create volume", func() { + // create a volume with blank config should work + _, err := volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + + vcc := entities.VolumeCreateOptions{ + Name: "foobar", + Label: nil, + Options: nil, + } + vol, err := volumes.Create(connText, vcc) + Expect(err).To(BeNil()) + Expect(vol.Name).To(Equal("foobar")) + + // create volume with same name should 500 + _, err = volumes.Create(connText, vcc) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("inspect volume", func() { + vol, err := volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + data, err := volumes.Inspect(connText, vol.Name) + Expect(err).To(BeNil()) + Expect(data.Name).To(Equal(vol.Name)) + }) + + It("remove volume", func() { + // removing a bogus volume should result in 404 + err := volumes.Remove(connText, "foobar", nil) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Removing an unused volume should work + vol, err := volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + err = volumes.Remove(connText, vol.Name, nil) + Expect(err).To(BeNil()) + + // Removing a volume that is being used without force should be 409 + vol, err = volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"}) + session.Wait(45) + err = volumes.Remove(connText, vol.Name, nil) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusConflict)) + + // Removing with a volume in use with force should work with a stopped container + zero := uint(0) + err = containers.Stop(connText, "vtest", &zero) + Expect(err).To(BeNil()) + err = volumes.Remove(connText, vol.Name, &bindings.PTrue) + Expect(err).To(BeNil()) + }) + + It("list volumes", func() { + // no volumes should be ok + vols, err := volumes.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeZero()) + + // create a bunch of named volumes and make verify with list + volNames := []string{"homer", "bart", "lisa", "maggie", "marge"} + for i := 0; i < 5; i++ { + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: volNames[i]}) + Expect(err).To(BeNil()) + } + vols, err = volumes.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 5)) + for _, v := range vols { + Expect(StringInSlice(v.Name, volNames)).To(BeTrue()) + } + + // list with bad filter should be 500 + filters := make(map[string][]string) + filters["foobar"] = []string{"1234"} + _, err = volumes.List(connText, filters) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + + filters = make(map[string][]string) + filters["name"] = []string{"homer"} + vols, err = volumes.List(connText, filters) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + Expect(vols[0].Name).To(Equal("homer")) + }) + + // TODO we need to add filtering to tests + It("prune unused volume", func() { + // Pruning when no volumes present should be ok + _, err := volumes.Prune(connText) + Expect(err).To(BeNil()) + + // Removing an unused volume should work + _, err = volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + vols, err := volumes.Prune(connText) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: "homer"}) + Expect(err).To(BeNil()) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"}) + session.Wait(45) + vols, err = volumes.Prune(connText) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + _, err = volumes.Inspect(connText, "homer") + Expect(err).To(BeNil()) + }) + +}) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 7f6a9cc9b..cef9246cb 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -5,56 +5,79 @@ import ( "net/http" "net/url" "strconv" + "strings" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" ) // Create creates a volume given its configuration. -func Create(ctx context.Context, config handlers.VolumeCreateConfig) (string, error) { - // TODO This is incomplete. The config needs to be sent via the body +func Create(ctx context.Context, config entities.VolumeCreateOptions) (*entities.VolumeConfigResponse, error) { var ( - volumeID string + v entities.VolumeConfigResponse ) conn, err := bindings.GetClient(ctx) if err != nil { - return "", err + return nil, err + } + createString, err := jsoniter.MarshalToString(config) + if err != nil { + return nil, err } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/create", nil) + stringReader := strings.NewReader(createString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/volumes/create", nil) if err != nil { - return volumeID, err + return nil, err } - return volumeID, response.Process(&volumeID) + return &v, response.Process(&v) } // Inspect returns low-level information about a volume. -func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, error) { +func Inspect(ctx context.Context, nameOrID string) (*entities.VolumeConfigResponse, error) { var ( - inspect libpod.InspectVolumeData + inspect entities.VolumeConfigResponse ) conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/json", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/volumes/%s/json", nil, nameOrID) if err != nil { return &inspect, err } return &inspect, response.Process(&inspect) } -func List() error { - // TODO - // The API side of things for this one does a lot in main and therefore - // is not implemented yet. - return bindings.ErrNotImplemented // nolint:typecheck +// List returns the configurations for existing volumes in the form of a slice. Optionally, filters +// can be used to refine the list of volumes. +func List(ctx context.Context, filters map[string][]string) ([]*entities.VolumeListReport, error) { + var ( + vols []*entities.VolumeListReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if len(filters) > 0 { + strFilters, err := bindings.FiltersToString(filters) + if err != nil { + return nil, err + } + params.Set("filters", strFilters) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/volumes/json", params) + if err != nil { + return vols, err + } + return vols, response.Process(&vols) } // Prune removes unused volumes from the local filesystem. -func Prune(ctx context.Context) ([]string, error) { +func Prune(ctx context.Context) ([]*entities.VolumePruneReport, error) { var ( - pruned []string + pruned []*entities.VolumePruneReport ) conn, err := bindings.GetClient(ctx) if err != nil { @@ -62,7 +85,7 @@ func Prune(ctx context.Context) ([]string, error) { } response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", nil) if err != nil { - return pruned, err + return nil, err } return pruned, response.Process(&pruned) } @@ -78,7 +101,7 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { if force != nil { params.Set("force", strconv.FormatBool(*force)) } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/prune", params, nameOrID) + response, err := conn.DoRequest(nil, http.MethodDelete, "/volumes/%s", params, nameOrID) if err != nil { return err } diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go deleted file mode 100644 index ea22498b8..000000000 --- a/pkg/capabilities/capabilities.go +++ /dev/null @@ -1,129 +0,0 @@ -package capabilities - -// Copyright 2013-2018 Docker, Inc. - -// NOTE: this package has been copied from github.com/docker/docker but been -// changed significantly to fit the needs of libpod. - -import ( - "strings" - - "github.com/containers/libpod/pkg/util" - "github.com/pkg/errors" - "github.com/syndtr/gocapability/capability" -) - -var ( - // Used internally and populated during init(). - capabilityList []string - - // ErrUnknownCapability is thrown when an unknown capability is processed. - ErrUnknownCapability = errors.New("unknown capability") -) - -// All is a special value used to add/drop all known capababilities. -// Useful on the CLI for `--cap-add=all` etc. -const All = "ALL" - -func init() { - last := capability.CAP_LAST_CAP - // hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap - if last == capability.Cap(63) { - last = capability.CAP_BLOCK_SUSPEND - } - for _, cap := range capability.List() { - if cap > last { - continue - } - capabilityList = append(capabilityList, "CAP_"+strings.ToUpper(cap.String())) - } -} - -// AllCapabilities returns all known capabilities. -func AllCapabilities() []string { - return capabilityList -} - -// normalizeCapabilities normalizes caps by adding a "CAP_" prefix (if not yet -// present). -func normalizeCapabilities(caps []string) ([]string, error) { - normalized := make([]string, len(caps)) - for i, c := range caps { - c = strings.ToUpper(c) - if c == All { - normalized = append(normalized, c) - continue - } - if !strings.HasPrefix(c, "CAP_") { - c = "CAP_" + c - } - if !util.StringInSlice(c, capabilityList) { - return nil, errors.Wrapf(ErrUnknownCapability, "%q", c) - } - normalized[i] = c - } - return normalized, nil -} - -// ValidateCapabilities validates if caps only contains valid capabilities. -func ValidateCapabilities(caps []string) error { - for _, c := range caps { - if !util.StringInSlice(c, capabilityList) { - return errors.Wrapf(ErrUnknownCapability, "%q", c) - } - } - return nil -} - -// MergeCapabilities computes a set of capabilities by adding capapbitilities -// to or dropping them from base. -// -// Note that "ALL" will cause all known capabilities to be added/dropped but -// the ones specified to be dropped/added. -func MergeCapabilities(base, adds, drops []string) ([]string, error) { - if len(adds) == 0 && len(drops) == 0 { - // Nothing to tweak; we're done - return base, nil - } - - capDrop, err := normalizeCapabilities(drops) - if err != nil { - return nil, err - } - capAdd, err := normalizeCapabilities(adds) - if err != nil { - return nil, err - } - - // Make sure that capDrop and capAdd are distinct sets. - for _, drop := range capDrop { - if util.StringInSlice(drop, capAdd) { - return nil, errors.Errorf("capability %q cannot be dropped and added", drop) - } - } - - var caps []string - - switch { - case util.StringInSlice(All, capAdd): - // Add all capabilities except ones on capDrop - for _, c := range capabilityList { - if !util.StringInSlice(c, capDrop) { - caps = append(caps, c) - } - } - case util.StringInSlice(All, capDrop): - // "Drop" all capabilities; use what's in capAdd instead - caps = capAdd - default: - // First drop some capabilities - for _, c := range base { - if !util.StringInSlice(c, capDrop) { - caps = append(caps, c) - } - } - // Then add the list of capabilities from capAdd - caps = append(caps, capAdd...) - } - return caps, nil -} diff --git a/pkg/cgroups/cgroups.go b/pkg/cgroups/cgroups.go index 6c5b7978c..d51905f4b 100644 --- a/pkg/cgroups/cgroups.go +++ b/pkg/cgroups/cgroups.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/containers/libpod/pkg/rootless" - systemdDbus "github.com/coreos/go-systemd/dbus" - "github.com/godbus/dbus" + systemdDbus "github.com/coreos/go-systemd/v22/dbus" + "github.com/godbus/dbus/v5" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" diff --git a/pkg/cgroups/systemd.go b/pkg/cgroups/systemd.go index b8e6db156..9bbdca415 100644 --- a/pkg/cgroups/systemd.go +++ b/pkg/cgroups/systemd.go @@ -5,8 +5,8 @@ import ( "path/filepath" "strings" - systemdDbus "github.com/coreos/go-systemd/dbus" - "github.com/godbus/dbus" + systemdDbus "github.com/coreos/go-systemd/v22/dbus" + "github.com/godbus/dbus/v5" ) func systemdCreate(path string, c *systemdDbus.Conn) error { diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go new file mode 100644 index 000000000..fbc0247ab --- /dev/null +++ b/pkg/domain/entities/containers.go @@ -0,0 +1,108 @@ +package entities + +import ( + "time" + + "github.com/containers/libpod/libpod/define" +) + +type WaitOptions struct { + Condition define.ContainerStatus + Interval time.Duration + Latest bool +} + +type WaitReport struct { + Id string + Error error + ExitCode int32 +} + +type BoolReport struct { + Value bool +} + +// StringSliceReport wraps a string slice. +type StringSliceReport struct { + Value []string +} + +type PauseUnPauseOptions struct { + All bool +} + +type PauseUnpauseReport struct { + Err error + Id string +} + +type StopOptions struct { + All bool + CIDFiles []string + Ignore bool + Latest bool + Timeout uint +} + +type StopReport struct { + Err error + Id string +} + +type TopOptions struct { + // CLI flags. + ListDescriptors bool + Latest bool + + // Options for the API. + Descriptors []string + NameOrID string +} + +type KillOptions struct { + All bool + Latest bool + Signal string +} + +type KillReport struct { + Err error + Id string +} + +type RestartOptions struct { + All bool + Latest bool + Running bool + Timeout *uint +} + +type RestartReport struct { + Err error + Id string +} + +type RmOptions struct { + All bool + CIDFiles []string + Force bool + Ignore bool + Latest bool + Storage bool + Volumes bool +} + +type RmReport struct { + Err error + Id string +} + +type ContainerInspectOptions struct { + Format string + Latest bool + Size bool +} + +type ContainerInspectReport struct { + *define.InspectContainerData +} diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go new file mode 100644 index 000000000..c14348529 --- /dev/null +++ b/pkg/domain/entities/engine.go @@ -0,0 +1,92 @@ +package entities + +import ( + "os/user" + "path/filepath" + + "github.com/containers/common/pkg/config" + "github.com/spf13/pflag" +) + +type EngineMode string + +const ( + ABIMode = EngineMode("abi") + TunnelMode = EngineMode("tunnel") +) + +func (m EngineMode) String() string { + return string(m) +} + +type EngineOptions struct { + Uri string + Identities []string + FlagSet *pflag.FlagSet + EngineMode EngineMode + + CGroupManager string + CniConfigDir string + ConmonPath string + DefaultMountsFile string + EventsBackend string + HooksDir []string + MaxWorks int + Namespace string + Root string + Runroot string + Runtime string + StorageDriver string + StorageOpts []string + Syslog bool + Trace bool + NetworkCmdPath string + + Config string + CpuProfile string + LogLevel string + TmpDir string + + RemoteUserName string + RemoteHost string + VarlinkAddress string + ConnectionName string + RemoteConfigFilePath string + Port int + IdentityFile string + IgnoreHosts bool +} + +func NewEngineOptions() (EngineOptions, error) { + u, _ := user.Current() + return EngineOptions{ + CGroupManager: config.SystemdCgroupsManager, + CniConfigDir: "", + Config: "", + ConmonPath: filepath.Join("usr", "bin", "conmon"), + ConnectionName: "", + CpuProfile: "", + DefaultMountsFile: "", + EventsBackend: "", + HooksDir: nil, + IdentityFile: "", + IgnoreHosts: false, + LogLevel: "", + MaxWorks: 0, + Namespace: "", + NetworkCmdPath: "", + Port: 0, + RemoteConfigFilePath: "", + RemoteHost: "", + RemoteUserName: "", + Root: "", + Runroot: filepath.Join("run", "user", u.Uid), + Runtime: "", + StorageDriver: "overlayfs", + StorageOpts: nil, + Syslog: false, + TmpDir: filepath.Join("run", "user", u.Uid, "libpod", "tmp"), + Trace: false, + VarlinkAddress: "", + }, nil +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go new file mode 100644 index 000000000..fceed1003 --- /dev/null +++ b/pkg/domain/entities/engine_container.go @@ -0,0 +1,33 @@ +package entities + +import ( + "context" +) + +type ContainerEngine interface { + ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) + ContainerInspect(ctx context.Context, namesOrIds []string, options ContainerInspectOptions) ([]*ContainerInspectReport, error) + ContainerKill(ctx context.Context, namesOrIds []string, options KillOptions) ([]*KillReport, error) + ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) + ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) + ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) + ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) + ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) + PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) + PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) + PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error) + PodPause(ctx context.Context, namesOrIds []string, options PodPauseOptions) ([]*PodPauseReport, error) + PodRestart(ctx context.Context, namesOrIds []string, options PodRestartOptions) ([]*PodRestartReport, error) + PodStart(ctx context.Context, namesOrIds []string, options PodStartOptions) ([]*PodStartReport, error) + PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) + PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) + PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) + + VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) + VolumeInspect(ctx context.Context, namesOrIds []string, opts VolumeInspectOptions) ([]*VolumeInspectReport, error) + VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) + VolumePrune(ctx context.Context, opts VolumePruneOptions) ([]*VolumePruneReport, error) + VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) +} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go new file mode 100644 index 000000000..d0c860a04 --- /dev/null +++ b/pkg/domain/entities/engine_image.go @@ -0,0 +1,13 @@ +package entities + +import ( + "context" +) + +type ImageEngine interface { + Delete(ctx context.Context, nameOrId []string, opts ImageDeleteOptions) (*ImageDeleteReport, error) + Exists(ctx context.Context, nameOrId string) (*BoolReport, error) + History(ctx context.Context, nameOrId string, opts ImageHistoryOptions) (*ImageHistoryReport, error) + List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error) + Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) +} diff --git a/pkg/domain/entities/filters.go b/pkg/domain/entities/filters.go new file mode 100644 index 000000000..c7e227244 --- /dev/null +++ b/pkg/domain/entities/filters.go @@ -0,0 +1,150 @@ +package entities + +import ( + "net/url" + "strings" +) + +// Identifier interface allows filters to access ID() of object +type Identifier interface { + Id() string +} + +// Named interface allows filters to access Name() of object +type Named interface { + Name() string +} + +// Named interface allows filters to access Name() of object +type Names interface { + Names() []string +} + +// IdOrName interface allows filters to access ID() or Name() of object +type IdOrNamed interface { + Identifier + Named +} + +// IdOrName interface allows filters to access ID() or Names() of object +type IdOrNames interface { + Identifier + Names +} + +type ImageFilter func(Image) bool +type VolumeFilter func(Volume) bool +type ContainerFilter func(Container) bool + +func CompileImageFilters(filters url.Values) ImageFilter { + var fns []interface{} + + for name, targets := range filters { + switch name { + case "id": + fns = append(fns, FilterIdFn(targets)) + case "name": + fns = append(fns, FilterNamesFn(targets)) + case "idOrName": + fns = append(fns, FilterIdOrNameFn(targets)) + } + } + + return func(image Image) bool { + for _, fn := range fns { + if !fn.(ImageFilter)(image) { + return false + } + } + return true + } +} + +func CompileContainerFilters(filters url.Values) ContainerFilter { + var fns []interface{} + + for name, targets := range filters { + switch name { + case "id": + fns = append(fns, FilterIdFn(targets)) + case "name": + fns = append(fns, FilterNameFn(targets)) + case "idOrName": + fns = append(fns, FilterIdOrNameFn(targets)) + } + } + + return func(ctnr Container) bool { + for _, fn := range fns { + if !fn.(ContainerFilter)(ctnr) { + return false + } + } + return true + } +} + +func CompileVolumeFilters(filters url.Values) VolumeFilter { + var fns []interface{} + + for name, targets := range filters { + if name == "id" { + fns = append(fns, FilterIdFn(targets)) + } + } + + return func(volume Volume) bool { + for _, fn := range fns { + if !fn.(VolumeFilter)(volume) { + return false + } + } + return true + } +} + +func FilterIdFn(id []string) func(Identifier) bool { + return func(obj Identifier) bool { + for _, v := range id { + if strings.Contains(obj.Id(), v) { + return true + } + } + return false + } +} + +func FilterNameFn(name []string) func(Named) bool { + return func(obj Named) bool { + for _, v := range name { + if strings.Contains(obj.Name(), v) { + return true + } + } + return false + } +} + +func FilterNamesFn(name []string) func(Names) bool { + return func(obj Names) bool { + for _, v := range name { + for _, n := range obj.Names() { + if strings.Contains(n, v) { + return true + } + } + } + return false + } +} + +func FilterIdOrNameFn(id []string) func(IdOrNamed) bool { + return func(obj IdOrNamed) bool { + for _, v := range id { + if strings.Contains(obj.Id(), v) || strings.Contains(obj.Name(), v) { + return true + } + } + return false + } +} diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go new file mode 100644 index 000000000..20af0356f --- /dev/null +++ b/pkg/domain/entities/images.go @@ -0,0 +1,139 @@ +package entities + +import ( + "net/url" + + "github.com/containers/image/v5/manifest" + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/opencontainers/go-digest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +type Image struct { + IdOrNamed + ID string `json:"Id"` + RepoTags []string `json:",omitempty"` + RepoDigests []string `json:",omitempty"` + Parent string `json:",omitempty"` + Comment string `json:",omitempty"` + Created string `json:",omitempty"` + Container string `json:",omitempty"` + ContainerConfig *container.Config `json:",omitempty"` + DockerVersion string `json:",omitempty"` + Author string `json:",omitempty"` + Config *container.Config `json:",omitempty"` + Architecture string `json:",omitempty"` + Variant string `json:",omitempty"` + Os string `json:",omitempty"` + OsVersion string `json:",omitempty"` + Size int64 `json:",omitempty"` + VirtualSize int64 `json:",omitempty"` + GraphDriver docker.GraphDriverData `json:",omitempty"` + RootFS docker.RootFS `json:",omitempty"` + Metadata docker.ImageMetadata `json:",omitempty"` + + // Podman extensions + Digest digest.Digest `json:",omitempty"` + PodmanVersion string `json:",omitempty"` + ManifestType string `json:",omitempty"` + User string `json:",omitempty"` + History []v1.History `json:",omitempty"` + NamesHistory []string `json:",omitempty"` + HealthCheck *manifest.Schema2HealthConfig `json:",omitempty"` +} + +func (i *Image) Id() string { + return i.ID +} + +type ImageSummary struct { + ID string `json:"Id"` + ParentId string `json:",omitempty"` + RepoTags []string `json:",omitempty"` + Created int64 `json:",omitempty"` + Size int64 `json:",omitempty"` + SharedSize int `json:",omitempty"` + VirtualSize int64 `json:",omitempty"` + Labels map[string]string `json:",omitempty"` + Containers int `json:",omitempty"` + ReadOnly bool `json:",omitempty"` + Dangling bool `json:",omitempty"` + + // Podman extensions + Names []string `json:",omitempty"` + Digest string `json:",omitempty"` + Digests []string `json:",omitempty"` + ConfigDigest string `json:",omitempty"` + History []string `json:",omitempty"` +} + +func (i *ImageSummary) Id() string { + return i.ID +} + +func (i *ImageSummary) IsReadOnly() bool { + return i.ReadOnly +} + +func (i *ImageSummary) IsDangling() bool { + return i.Dangling +} + +type ImageDeleteOptions struct { + All bool + Force bool +} + +// ImageDeleteResponse is the response for removing one or more image(s) from storage +// and containers what was untagged vs actually removed +type ImageDeleteReport struct { + Untagged []string `json:",omitempty"` + Deleted []string `json:",omitempty"` + Errors []error + ImageNotFound error + ImageInUse error +} + +type ImageHistoryOptions struct{} + +type ImageHistoryLayer struct { + ID string `json:"Id"` + Created int64 `json:",omitempty"` + CreatedBy string `json:",omitempty"` + Tags []string `json:",omitempty"` + Size int64 `json:",omitempty"` + Comment string `json:",omitempty"` +} + +type ImageHistoryReport struct { + Layers []ImageHistoryLayer +} + +type ImageInspectOptions struct { + TypeObject string `json:",omitempty"` + Format string `json:",omitempty"` + Size bool `json:",omitempty"` + Latest bool `json:",omitempty"` +} + +type ImageListOptions struct { + All bool `json:"all" schema:"all"` + Filter []string `json:"Filter,omitempty"` + Filters url.Values `json:"filters" schema:"filters"` +} + +// type ImageListReport struct { +// Images []ImageSummary +// } + +type ImagePruneOptions struct { + All bool `json:"all" schema:"all"` + Filter []string `json:"filter" schema:"filter"` + Filters url.Values `json:"filters" schema:"filters"` +} + +type ImagePruneReport struct { + Report Report + Size int64 +} diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go new file mode 100644 index 000000000..efda17d65 --- /dev/null +++ b/pkg/domain/entities/pods.go @@ -0,0 +1,143 @@ +package entities + +import ( + "time" + + "github.com/containers/libpod/pkg/specgen" +) + +type PodKillOptions struct { + All bool + Latest bool + Signal string +} + +type PodKillReport struct { + Errs []error + Id string +} + +type ListPodsReport struct { + Cgroup string + Containers []*ListPodContainer + Created time.Time + Id string + Name string + Namespace string + Status string +} + +type ListPodContainer struct { + Id string + Names string + Status string +} + +type PodPauseOptions struct { + All bool + Latest bool +} + +type PodPauseReport struct { + Errs []error + Id string +} + +type PodunpauseOptions struct { + All bool + Latest bool +} + +type PodUnpauseReport struct { + Errs []error + Id string +} + +type PodStopOptions struct { + All bool + Ignore bool + Latest bool + Timeout int +} + +type PodStopReport struct { + Errs []error + Id string +} + +type PodRestartOptions struct { + All bool + Latest bool +} + +type PodRestartReport struct { + Errs []error + Id string +} + +type PodStartOptions struct { + All bool + Latest bool +} + +type PodStartReport struct { + Errs []error + Id string +} + +type PodRmOptions struct { + All bool + Force bool + Ignore bool + Latest bool +} + +type PodRmReport struct { + Err error + Id string +} + +type PodCreateOptions struct { + CGroupParent string + Hostname string + Infra bool + InfraImage string + InfraCommand string + Labels map[string]string + Name string + Net *NetOptions + Share []string +} + +type PodCreateReport struct { + Id string +} + +func (p PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) { + // Basic Config + s.Name = p.Name + s.Hostname = p.Hostname + s.Labels = p.Labels + s.NoInfra = !p.Infra + s.InfraCommand = []string{p.InfraCommand} + s.InfraImage = p.InfraImage + s.SharedNamespaces = p.Share + + // Networking config + s.NetNS = p.Net.Network + s.StaticIP = p.Net.StaticIP + s.StaticMAC = p.Net.StaticMAC + s.PortMappings = p.Net.PublishPorts + s.CNINetworks = p.Net.CNINetworks + if p.Net.DNSHost { + s.NoManageResolvConf = true + } + s.DNSServer = p.Net.DNSServers + s.DNSSearch = p.Net.DNSSearch + s.DNSOption = p.Net.DNSOptions + s.NoManageHosts = p.Net.NoHosts + s.HostAdd = p.Net.AddHosts + + // Cgroup + s.CgroupParent = p.CGroupParent +} diff --git a/pkg/domain/entities/set.go b/pkg/domain/entities/set.go new file mode 100644 index 000000000..c8d6cb1a9 --- /dev/null +++ b/pkg/domain/entities/set.go @@ -0,0 +1,45 @@ +package entities + +import ( + "strings" +) + +type stringSet struct { + m map[string]struct{} +} + +func NewStringSet(elem ...string) *stringSet { + s := &stringSet{} + s.m = make(map[string]struct{}, len(elem)) + for _, e := range elem { + s.Add(e) + } + return s +} + +func (s *stringSet) Add(elem string) { + s.m[elem] = struct{}{} +} + +func (s *stringSet) Remove(elem string) { + delete(s.m, elem) +} + +func (s *stringSet) Contains(elem string) bool { + _, ok := s.m[elem] + return ok +} + +func (s *stringSet) Elements() []string { + keys := make([]string, len(s.m)) + i := 0 + for k := range s.m { + keys[i] = k + i++ + } + return keys +} + +func (s *stringSet) String() string { + return strings.Join(s.Elements(), ", ") +} diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go new file mode 100644 index 000000000..a1a729584 --- /dev/null +++ b/pkg/domain/entities/types.go @@ -0,0 +1,44 @@ +package entities + +import ( + "net" + + "github.com/containers/libpod/pkg/specgen" + "github.com/cri-o/ocicni/pkg/ocicni" +) + +type Container struct { + IdOrNamed +} + +type Volume struct { + Identifier +} + +type Report struct { + Id []string + Err map[string]error +} + +type PodDeleteReport struct{ Report } +type PodPruneOptions struct{} + +type PodPruneReport struct{ Report } +type VolumeDeleteOptions struct{} +type VolumeDeleteReport struct{ Report } + +// NetOptions reflect the shared network options between +// pods and containers +type NetOptions struct { + AddHosts []string + CNINetworks []string + DNSHost bool + DNSOptions []string + DNSSearch []string + DNSServers []net.IP + Network specgen.Namespace + NoHosts bool + PublishPorts []ocicni.PortMapping + StaticIP *net.IP + StaticMAC *net.HardwareAddr +} diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go new file mode 100644 index 000000000..23c066083 --- /dev/null +++ b/pkg/domain/entities/volumes.go @@ -0,0 +1,92 @@ +package entities + +import ( + "time" +) + +// swagger:model VolumeCreate +type VolumeCreateOptions struct { + // New volume's name. Can be left blank + Name string `schema:"name"` + // Volume driver to use + Driver string `schema:"driver"` + // User-defined key/value metadata. + Label map[string]string `schema:"label"` + // Mapping of driver options and values. + Options map[string]string `schema:"opts"` +} + +type IdOrNameResponse struct { + // The Id or Name of an object + IdOrName string +} + +type VolumeConfigResponse struct { + // Name is the name of the volume. + Name string `json:"Name"` + // Driver is the driver used to create the volume. + // This will be properly implemented in a future version. + Driver string `json:"Driver"` + // Mountpoint is the path on the host where the volume is mounted. + Mountpoint string `json:"Mountpoint"` + // CreatedAt is the date and time the volume was created at. This is not + // stored for older Libpod volumes; if so, it will be omitted. + CreatedAt time.Time `json:"CreatedAt,omitempty"` + // Status is presently unused and provided only for Docker compatibility. + // In the future it will be used to return information on the volume's + // current state. + Status map[string]string `json:"Status,omitempty"` + // Labels includes the volume's configured labels, key:value pairs that + // can be passed during volume creation to provide information for third + // party tools. + Labels map[string]string `json:"Labels"` + // Scope is unused and provided solely for Docker compatibility. It is + // unconditionally set to "local". + Scope string `json:"Scope"` + // Options is a set of options that were used when creating the volume. + // It is presently not used. + Options map[string]string `json:"Options"` + // UID is the UID that the volume was created with. + UID int `json:"UID,omitempty"` + // GID is the GID that the volume was created with. + GID int `json:"GID,omitempty"` + // Anonymous indicates that the volume was created as an anonymous + // volume for a specific container, and will be be removed when any + // container using it is removed. + Anonymous bool `json:"Anonymous,omitempty"` +} + +type VolumeRmOptions struct { + All bool + Force bool +} + +type VolumeRmReport struct { + Err error + Id string +} + +type VolumeInspectOptions struct { + All bool +} + +type VolumeInspectReport struct { + *VolumeConfigResponse +} + +type VolumePruneOptions struct { + Force bool +} + +type VolumePruneReport struct { + Err error + Id string +} + +type VolumeListOptions struct { + Filter map[string][]string +} + +type VolumeListReport struct { + VolumeConfigResponse +} diff --git a/pkg/domain/filters/volumes.go b/pkg/domain/filters/volumes.go new file mode 100644 index 000000000..f97c3f570 --- /dev/null +++ b/pkg/domain/filters/volumes.go @@ -0,0 +1,70 @@ +package filters + +import ( + "strings" + + "github.com/containers/libpod/libpod" + "github.com/pkg/errors" +) + +func GenerateVolumeFilters(filters map[string][]string) ([]libpod.VolumeFilter, error) { + var vf []libpod.VolumeFilter + for filter, v := range filters { + for _, val := range v { + switch filter { + case "name": + nameVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return nameVal == v.Name() + }) + case "driver": + driverVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return v.Driver() == driverVal + }) + case "scope": + scopeVal := val + vf = append(vf, func(v *libpod.Volume) bool { + return v.Scope() == scopeVal + }) + case "label": + filterArray := strings.SplitN(val, "=", 2) + filterKey := filterArray[0] + var filterVal string + if len(filterArray) > 1 { + filterVal = filterArray[1] + } else { + filterVal = "" + } + vf = append(vf, func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Labels() { + if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { + return true + } + } + return false + }) + case "opt": + filterArray := strings.SplitN(val, "=", 2) + filterKey := filterArray[0] + var filterVal string + if len(filterArray) > 1 { + filterVal = filterArray[1] + } else { + filterVal = "" + } + vf = append(vf, func(v *libpod.Volume) bool { + for labelKey, labelValue := range v.Options() { + if labelKey == filterKey && ("" == filterVal || labelValue == filterVal) { + return true + } + } + return false + }) + default: + return nil, errors.Errorf("%q is in an invalid volume filter", filter) + } + } + } + return vf, nil +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go new file mode 100644 index 000000000..3965c5f75 --- /dev/null +++ b/pkg/domain/infra/abi/containers.go @@ -0,0 +1,279 @@ +// +build ABISupport + +package abi + +import ( + "context" + "io/ioutil" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/signal" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// TODO: Should return *entities.ContainerExistsReport, error +func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + _, err := ic.Libpod.LookupContainer(nameOrId) + if err != nil && errors.Cause(err) != define.ErrNoSuchCtr { + return nil, err + } + return &entities.BoolReport{Value: err == nil}, nil +} + +func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, options entities.WaitOptions) ([]entities.WaitReport, error) { + var ( + responses []entities.WaitReport + ) + ctrs, err := shortcuts.GetContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, c := range ctrs { + response := entities.WaitReport{Id: c.ID()} + exitCode, err := c.WaitForConditionWithInterval(options.Interval, options.Condition) + if err != nil { + response.Error = err + } else { + response.ExitCode = exitCode + } + responses = append(responses, response) + } + return responses, nil +} + +func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + ctrs []*libpod.Container + err error + report []*entities.PauseUnpauseReport + ) + if options.All { + ctrs, err = ic.Libpod.GetAllContainers() + } else { + ctrs, err = shortcuts.GetContainersByContext(false, false, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := c.Pause() + report = append(report, &entities.PauseUnpauseReport{Id: c.ID(), Err: err}) + } + return report, nil +} + +func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + ctrs []*libpod.Container + err error + report []*entities.PauseUnpauseReport + ) + if options.All { + ctrs, err = ic.Libpod.GetAllContainers() + } else { + ctrs, err = shortcuts.GetContainersByContext(false, false, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := c.Unpause() + report = append(report, &entities.PauseUnpauseReport{Id: c.ID(), Err: err}) + } + return report, nil +} +func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { + var ( + reports []*entities.StopReport + ) + names := namesOrIds + for _, cidFile := range options.CIDFiles { + content, err := ioutil.ReadFile(cidFile) + if err != nil { + return nil, errors.Wrap(err, "error reading CIDFile") + } + id := strings.Split(string(content), "\n")[0] + names = append(names, id) + } + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, names, ic.Libpod) + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + return nil, err + } + for _, con := range ctrs { + report := entities.StopReport{Id: con.ID()} + err = con.StopWithTimeout(options.Timeout) + if err != nil { + // These first two are considered non-fatal under the right conditions + if errors.Cause(err) == define.ErrCtrStopped { + logrus.Debugf("Container %s is already stopped", con.ID()) + reports = append(reports, &report) + continue + + } else if options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + logrus.Debugf("Container %s is not running, could not stop", con.ID()) + reports = append(reports, &report) + continue + } + report.Err = err + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, options entities.KillOptions) ([]*entities.KillReport, error) { + var ( + reports []*entities.KillReport + ) + sig, err := signal.ParseSignalNameOrNumber(options.Signal) + if err != nil { + return nil, err + } + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, con := range ctrs { + reports = append(reports, &entities.KillReport{ + Id: con.ID(), + Err: con.Kill(uint(sig)), + }) + } + return reports, nil +} +func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { + var ( + reports []*entities.RestartReport + ) + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, con := range ctrs { + timeout := con.StopTimeout() + if options.Timeout != nil { + timeout = *options.Timeout + } + reports = append(reports, &entities.RestartReport{ + Id: con.ID(), + Err: con.RestartWithTimeout(ctx, timeout), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*entities.RmReport, error) { + var ( + reports []*entities.RmReport + ) + if options.Storage { + for _, ctr := range namesOrIds { + report := entities.RmReport{Id: ctr} + if err := ic.Libpod.RemoveStorageContainer(ctr, options.Force); err != nil { + report.Err = err + } + reports = append(reports, &report) + } + return reports, nil + } + + names := namesOrIds + for _, cidFile := range options.CIDFiles { + content, err := ioutil.ReadFile(cidFile) + if err != nil { + return nil, errors.Wrap(err, "error reading CIDFile") + } + id := strings.Split(string(content), "\n")[0] + names = append(names, id) + } + + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, names, ic.Libpod) + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + // Failed to get containers. If force is specified, get the containers ID + // and evict them + if !options.Force { + return nil, err + } + + for _, ctr := range namesOrIds { + logrus.Debugf("Evicting container %q", ctr) + report := entities.RmReport{Id: ctr} + id, err := ic.Libpod.EvictContainer(ctx, ctr, options.Volumes) + if err != nil { + if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { + logrus.Debugf("Ignoring error (--allow-missing): %v", err) + reports = append(reports, &report) + continue + } + report.Err = errors.Wrapf(err, "Failed to evict container: %q", id) + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil + } + + for _, c := range ctrs { + report := entities.RmReport{Id: c.ID()} + err := ic.Libpod.RemoveContainer(ctx, c, options.Force, options.Volumes) + if err != nil { + if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { + logrus.Debugf("Ignoring error (--allow-missing): %v", err) + reports = append(reports, &report) + continue + } + logrus.Debugf("Failed to remove container %s: %s", c.ID(), err.Error()) + report.Err = err + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.ContainerInspectOptions) ([]*entities.ContainerInspectReport, error) { + var reports []*entities.ContainerInspectReport + ctrs, err := shortcuts.GetContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, c := range ctrs { + data, err := c.Inspect(options.Size) + if err != nil { + return nil, err + } + reports = append(reports, &entities.ContainerInspectReport{InspectContainerData: data}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerTop(ctx context.Context, options entities.TopOptions) (*entities.StringSliceReport, error) { + var ( + container *libpod.Container + err error + ) + + // Look up the container. + if options.Latest { + container, err = ic.Libpod.GetLatestContainer() + } else { + container, err = ic.Libpod.LookupContainer(options.NameOrID) + } + if err != nil { + return nil, errors.Wrap(err, "unable to lookup requested container") + } + + // Run Top. + report := &entities.StringSliceReport{} + report.Value, err = container.Top(options.Descriptors) + return report, err +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go new file mode 100644 index 000000000..44420c1e1 --- /dev/null +++ b/pkg/domain/infra/abi/images.go @@ -0,0 +1,166 @@ +// +build ABISupport + +package abi + +import ( + "context" + "fmt" + + libpodImage "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/storage" + "github.com/pkg/errors" +) + +func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { + if _, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId); err != nil { + return &entities.BoolReport{}, nil + } + return &entities.BoolReport{Value: true}, nil +} + +func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { + report := entities.ImageDeleteReport{} + + if opts.All { + var previousTargets []*libpodImage.Image + repeatRun: + targets, err := ir.Libpod.ImageRuntime().GetRWImages() + if err != nil { + return &report, errors.Wrapf(err, "unable to query local images") + } + + if len(targets) > 0 && len(targets) == len(previousTargets) { + return &report, errors.New("unable to delete all images; re-run the rmi command again.") + } + previousTargets = targets + + for _, img := range targets { + isParent, err := img.IsParent(ctx) + if err != nil { + return &report, err + } + if isParent { + continue + } + err = ir.deleteImage(ctx, img, opts, report) + report.Errors = append(report.Errors, err) + } + if len(previousTargets) != 1 { + goto repeatRun + } + return &report, nil + } + + for _, id := range nameOrId { + image, err := ir.Libpod.ImageRuntime().NewFromLocal(id) + if err != nil { + return nil, err + } + + err = ir.deleteImage(ctx, image, opts, report) + if err != nil { + return &report, err + } + } + return &report, nil +} + +func (ir *ImageEngine) deleteImage(ctx context.Context, img *libpodImage.Image, opts entities.ImageDeleteOptions, report entities.ImageDeleteReport) error { + results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) + switch errors.Cause(err) { + case nil: + break + case storage.ErrImageUsedByContainer: + report.ImageInUse = errors.New( + fmt.Sprintf("A container associated with containers/storage, i.e. via Buildah, CRI-O, etc., may be associated with this image: %-12.12s\n", img.ID())) + return nil + case libpodImage.ErrNoSuchImage: + report.ImageNotFound = err + return nil + default: + return err + } + + report.Deleted = append(report.Deleted, results.Deleted) + report.Untagged = append(report.Untagged, results.Untagged...) + return nil +} + +func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { + results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, opts.Filter) + if err != nil { + return nil, err + } + + report := entities.ImagePruneReport{ + Report: entities.Report{ + Id: results, + Err: nil, + }, + Size: 0, + } + return &report, nil +} + +func (ir *ImageEngine) History(ctx context.Context, nameOrId string, opts entities.ImageHistoryOptions) (*entities.ImageHistoryReport, error) { + image, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) + if err != nil { + return nil, err + } + results, err := image.History(ctx) + if err != nil { + return nil, err + } + + history := entities.ImageHistoryReport{ + Layers: make([]entities.ImageHistoryLayer, len(results)), + } + + for i, layer := range results { + history.Layers[i] = ToDomainHistoryLayer(layer) + } + return &history, nil +} + +func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer { + l := entities.ImageHistoryLayer{} + l.ID = layer.ID + l.Created = layer.Created.Unix() + l.CreatedBy = layer.CreatedBy + copy(l.Tags, layer.Tags) + l.Size = layer.Size + l.Comment = layer.Comment + return l +} + +// func (r *imageRuntime) Delete(ctx context.Context, nameOrId string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { +// image, err := r.libpod.ImageEngine().NewFromLocal(nameOrId) +// if err != nil { +// return nil, err +// } +// +// results, err := r.libpod.RemoveImage(ctx, image, opts.Force) +// if err != nil { +// return nil, err +// } +// +// report := entities.ImageDeleteReport{} +// if err := utils.DeepCopy(&report, results); err != nil { +// return nil, err +// } +// return &report, nil +// } +// +// func (r *imageRuntime) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { +// // TODO: map FilterOptions +// id, err := r.libpod.ImageEngine().PruneImages(ctx, opts.All, []string{}) +// if err != nil { +// return nil, err +// } +// +// // TODO: Determine Size +// report := entities.ImagePruneReport{} +// copy(report.Report.Id, id) +// return &report, nil +// } diff --git a/pkg/domain/infra/abi/images_list.go b/pkg/domain/infra/abi/images_list.go new file mode 100644 index 000000000..2f4020374 --- /dev/null +++ b/pkg/domain/infra/abi/images_list.go @@ -0,0 +1,80 @@ +// +build ABISupport + +package abi + +import ( + "context" + + libpodImage "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) ([]*entities.ImageSummary, error) { + var ( + images []*libpodImage.Image + err error + ) + + // TODO: Future work support for domain.Filters + // filters := utils.ToLibpodFilters(opts.Filters) + + if len(opts.Filter) > 0 { + images, err = ir.Libpod.ImageRuntime().GetImagesWithFilters(opts.Filter) + } else { + images, err = ir.Libpod.ImageRuntime().GetImages() + } + if err != nil { + return nil, err + } + + summaries := make([]*entities.ImageSummary, len(images)) + for i, img := range images { + var repoTags []string + if opts.All { + pairs, err := libpodImage.ReposToMap(img.Names()) + if err != nil { + return nil, err + } + + for repo, tags := range pairs { + for _, tag := range tags { + repoTags = append(repoTags, repo+":"+tag) + } + } + } else { + repoTags, _ = img.RepoTags() + } + + digests := make([]string, len(img.Digests())) + for j, d := range img.Digests() { + digests[j] = string(d) + } + + e := entities.ImageSummary{ + ID: img.ID(), + + ConfigDigest: string(img.ConfigDigest), + Created: img.Created().Unix(), + Dangling: img.Dangling(), + Digest: string(img.Digest()), + Digests: digests, + History: img.NamesHistory(), + Names: img.Names(), + ParentId: img.Parent, + ReadOnly: img.IsReadOnly(), + SharedSize: 0, + VirtualSize: img.VirtualSize, + RepoTags: repoTags, + } + e.Labels, _ = img.Labels(context.TODO()) + + ctnrs, _ := img.Containers() + e.Containers = len(ctnrs) + + sz, _ := img.Size(context.TODO()) + e.Size = int64(*sz) + + summaries[i] = &e + } + return summaries, nil +} diff --git a/pkg/domain/infra/abi/images_test.go b/pkg/domain/infra/abi/images_test.go new file mode 100644 index 000000000..20ef1b150 --- /dev/null +++ b/pkg/domain/infra/abi/images_test.go @@ -0,0 +1,37 @@ +package abi + +// +// import ( +// "context" +// "testing" +// +// "github.com/stretchr/testify/mock" +// ) +// +// type MockImageRuntime struct { +// mock.Mock +// } +// +// func (m *MockImageRuntime) Delete(ctx context.Context, renderer func() interface{}, name string) error { +// _ = m.Called(ctx, renderer, name) +// return nil +// } +// +// func TestImageSuccess(t *testing.T) { +// actual := func() interface{} { return nil } +// +// m := new(MockImageRuntime) +// m.On( +// "Delete", +// mock.AnythingOfType("*context.emptyCtx"), +// mock.AnythingOfType("func() interface {}"), +// "fedora"). +// Return(nil) +// +// r := DirectImageRuntime{m} +// err := r.Delete(context.TODO(), actual, "fedora") +// if err != nil { +// t.Errorf("error should be nil, got: %v", err) +// } +// m.AssertExpectations(t) +// } diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go new file mode 100644 index 000000000..6c0e1ee55 --- /dev/null +++ b/pkg/domain/infra/abi/parse/parse.go @@ -0,0 +1,68 @@ +package parse + +import ( + "strconv" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Handle volume options from CLI. +// Parse "o" option to find UID, GID. +func ParseVolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) { + libpodOptions := []libpod.VolumeCreateOption{} + volumeOptions := make(map[string]string) + + for key, value := range opts { + switch key { + case "o": + // o has special handling to parse out UID, GID. + // These are separate Libpod options. + splitVal := strings.Split(value, ",") + finalVal := []string{} + for _, o := range splitVal { + // Options will be formatted as either "opt" or + // "opt=value" + splitO := strings.SplitN(o, "=", 2) + switch strings.ToLower(splitO[0]) { + case "uid": + if len(splitO) != 2 { + return nil, errors.Wrapf(define.ErrInvalidArg, "uid option must provide a UID") + } + intUID, err := strconv.Atoi(splitO[1]) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert UID %s to integer", splitO[1]) + } + logrus.Debugf("Removing uid= from options and adding WithVolumeUID for UID %d", intUID) + libpodOptions = append(libpodOptions, libpod.WithVolumeUID(intUID)) + case "gid": + if len(splitO) != 2 { + return nil, errors.Wrapf(define.ErrInvalidArg, "gid option must provide a GID") + } + intGID, err := strconv.Atoi(splitO[1]) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert GID %s to integer", splitO[1]) + } + logrus.Debugf("Removing gid= from options and adding WithVolumeGID for GID %d", intGID) + libpodOptions = append(libpodOptions, libpod.WithVolumeGID(intGID)) + default: + finalVal = append(finalVal, o) + } + } + if len(finalVal) > 0 { + volumeOptions[key] = strings.Join(finalVal, ",") + } + default: + volumeOptions[key] = value + } + } + + if len(volumeOptions) > 0 { + libpodOptions = append(libpodOptions, libpod.WithVolumeOptions(volumeOptions)) + } + + return libpodOptions, nil +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go new file mode 100644 index 000000000..619e973cf --- /dev/null +++ b/pkg/domain/infra/abi/pods.go @@ -0,0 +1,252 @@ +// +build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/signal" + "github.com/containers/libpod/pkg/specgen" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// getPodsByContext returns a slice of pods. Note that all, latest and pods are +// mutually exclusive arguments. +func getPodsByContext(all, latest bool, pods []string, runtime *libpod.Runtime) ([]*libpod.Pod, error) { + var outpods []*libpod.Pod + if all { + return runtime.GetAllPods() + } + if latest { + p, err := runtime.GetLatestPod() + if err != nil { + return nil, err + } + outpods = append(outpods, p) + return outpods, nil + } + var err error + for _, p := range pods { + pod, e := runtime.LookupPod(p) + if e != nil { + // Log all errors here, so callers don't need to. + logrus.Debugf("Error looking up pod %q: %v", p, e) + if err == nil { + err = e + } + } else { + outpods = append(outpods, pod) + } + } + return outpods, err +} + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + _, err := ic.Libpod.LookupPod(nameOrId) + if err != nil && errors.Cause(err) != define.ErrNoSuchPod { + return nil, err + } + return &entities.BoolReport{Value: err == nil}, nil +} + +func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, options entities.PodKillOptions) ([]*entities.PodKillReport, error) { + var ( + reports []*entities.PodKillReport + ) + sig, err := signal.ParseSignalNameOrNumber(options.Signal) + if err != nil { + return nil, err + } + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + + for _, p := range pods { + report := entities.PodKillReport{Id: p.ID()} + conErrs, err := p.Kill(uint(sig)) + if err != nil { + report.Errs = []error{err} + reports = append(reports, &report) + continue + } + if len(conErrs) > 0 { + for _, err := range conErrs { + report.Errs = append(report.Errs, err) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) { + var ( + reports []*entities.PodPauseReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodPauseReport{Id: p.ID()} + errs, err := p.Pause() + if err != nil { + report.Errs = []error{err} + continue + } + if len(errs) > 0 { + for _, v := range errs { + report.Errs = append(report.Errs, v) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodUnpause(ctx context.Context, namesOrIds []string, options entities.PodunpauseOptions) ([]*entities.PodUnpauseReport, error) { + var ( + reports []*entities.PodUnpauseReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodUnpauseReport{Id: p.ID()} + errs, err := p.Unpause() + if err != nil { + report.Errs = []error{err} + continue + } + if len(errs) > 0 { + for _, v := range errs { + report.Errs = append(report.Errs, v) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodStop(ctx context.Context, namesOrIds []string, options entities.PodStopOptions) ([]*entities.PodStopReport, error) { + var ( + reports []*entities.PodStopReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodStopReport{Id: p.ID()} + errs, err := p.StopWithTimeout(ctx, false, options.Timeout) + if err != nil { + report.Errs = []error{err} + continue + } + if len(errs) > 0 { + for _, v := range errs { + report.Errs = append(report.Errs, v) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodRestart(ctx context.Context, namesOrIds []string, options entities.PodRestartOptions) ([]*entities.PodRestartReport, error) { + var ( + reports []*entities.PodRestartReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodRestartReport{Id: p.ID()} + errs, err := p.Restart(ctx) + if err != nil { + report.Errs = []error{err} + continue + } + if len(errs) > 0 { + for _, v := range errs { + report.Errs = append(report.Errs, v) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodStart(ctx context.Context, namesOrIds []string, options entities.PodStartOptions) ([]*entities.PodStartReport, error) { + var ( + reports []*entities.PodStartReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodStartReport{Id: p.ID()} + errs, err := p.Start(ctx) + if err != nil { + report.Errs = []error{err} + continue + } + if len(errs) > 0 { + for _, v := range errs { + report.Errs = append(report.Errs, v) + } + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, options entities.PodRmOptions) ([]*entities.PodRmReport, error) { + var ( + reports []*entities.PodRmReport + ) + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, p := range pods { + report := entities.PodRmReport{Id: p.ID()} + err := ic.Libpod.RemovePod(ctx, p, true, options.Force) + if err != nil { + report.Err = err + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) PodCreate(ctx context.Context, opts entities.PodCreateOptions) (*entities.PodCreateReport, error) { + podSpec := specgen.NewPodSpecGenerator() + opts.ToPodSpecGen(podSpec) + pod, err := podSpec.MakePod(ic.Libpod) + if err != nil { + return nil, err + } + return &entities.PodCreateReport{Id: pod.ID()}, nil +} diff --git a/pkg/domain/infra/abi/runtime.go b/pkg/domain/infra/abi/runtime.go new file mode 100644 index 000000000..b53fb6d3a --- /dev/null +++ b/pkg/domain/infra/abi/runtime.go @@ -0,0 +1,17 @@ +// +build ABISupport + +package abi + +import ( + "github.com/containers/libpod/libpod" +) + +// Image-related runtime linked against libpod library +type ImageEngine struct { + Libpod *libpod.Runtime +} + +// Container-related runtime linked against libpod library +type ContainerEngine struct { + Libpod *libpod.Runtime +} diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go new file mode 100644 index 000000000..bdae4359d --- /dev/null +++ b/pkg/domain/infra/abi/volumes.go @@ -0,0 +1,159 @@ +// +build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/filters" + "github.com/containers/libpod/pkg/domain/infra/abi/parse" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) { + var ( + volumeOptions []libpod.VolumeCreateOption + ) + if len(opts.Name) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeName(opts.Name)) + } + if len(opts.Driver) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeDriver(opts.Driver)) + } + if len(opts.Label) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(opts.Label)) + } + if len(opts.Options) > 0 { + parsedOptions, err := parse.ParseVolumeOptions(opts.Options) + if err != nil { + return nil, err + } + volumeOptions = append(volumeOptions, parsedOptions...) + } + vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) + if err != nil { + return nil, err + } + return &entities.IdOrNameResponse{IdOrName: vol.Name()}, nil +} + +func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, opts entities.VolumeRmOptions) ([]*entities.VolumeRmReport, error) { + var ( + err error + reports []*entities.VolumeRmReport + vols []*libpod.Volume + ) + if opts.All { + vols, err = ic.Libpod.Volumes() + if err != nil { + return nil, err + } + } else { + for _, id := range namesOrIds { + vol, err := ic.Libpod.LookupVolume(id) + if err != nil { + reports = append(reports, &entities.VolumeRmReport{ + Err: err, + Id: id, + }) + continue + } + vols = append(vols, vol) + } + } + for _, vol := range vols { + reports = append(reports, &entities.VolumeRmReport{ + Err: ic.Libpod.RemoveVolume(ctx, vol, opts.Force), + Id: vol.Name(), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []string, opts entities.VolumeInspectOptions) ([]*entities.VolumeInspectReport, error) { + var ( + err error + reports []*entities.VolumeInspectReport + vols []*libpod.Volume + ) + + // Note: as with previous implementation, a single failure here + // results a return. + if opts.All { + vols, err = ic.Libpod.GetAllVolumes() + if err != nil { + return nil, err + } + } else { + for _, v := range namesOrIds { + vol, err := ic.Libpod.LookupVolume(v) + if err != nil { + return nil, errors.Wrapf(err, "error inspecting volume %s", v) + } + vols = append(vols, vol) + } + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + reports = append(reports, &entities.VolumeInspectReport{VolumeConfigResponse: &config}) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + var ( + reports []*entities.VolumePruneReport + ) + pruned, err := ic.Libpod.PruneVolumes(ctx) + if err != nil { + return nil, err + } + for k, v := range pruned { + reports = append(reports, &entities.VolumePruneReport{ + Err: v, + Id: k, + }) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { + var ( + reports []*entities.VolumeListReport + ) + volumeFilters, err := filters.GenerateVolumeFilters(opts.Filter) + if err != nil { + return nil, err + } + vols, err := ic.Libpod.Volumes(volumeFilters...) + if err != nil { + return nil, err + } + for _, v := range vols { + config := entities.VolumeConfigResponse{ + Name: v.Name(), + Driver: v.Driver(), + Mountpoint: v.MountPoint(), + CreatedAt: v.CreatedTime(), + Labels: v.Labels(), + Scope: v.Scope(), + Options: v.Options(), + UID: v.UID(), + GID: v.GID(), + } + reports = append(reports, &entities.VolumeListReport{VolumeConfigResponse: config}) + } + return reports, nil +} diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go new file mode 100644 index 000000000..f11026571 --- /dev/null +++ b/pkg/domain/infra/runtime_abi.go @@ -0,0 +1,38 @@ +// +build ABISupport + +package infra + +import ( + "context" + "fmt" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/tunnel" +) + +// NewContainerEngine factory provides a libpod runtime for container-related operations +func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, error) { + switch facts.EngineMode { + case entities.ABIMode: + r, err := NewLibpodRuntime(facts.FlagSet, facts) + return r, err + case entities.TunnelMode: + ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + return &tunnel.ContainerEngine{ClientCxt: ctx}, err + } + return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) +} + +// NewContainerEngine factory provides a libpod runtime for image-related operations +func NewImageEngine(facts entities.EngineOptions) (entities.ImageEngine, error) { + switch facts.EngineMode { + case entities.ABIMode: + r, err := NewLibpodImageRuntime(facts.FlagSet, facts) + return r, err + case entities.TunnelMode: + ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + return &tunnel.ImageEngine{ClientCxt: ctx}, err + } + return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) +} diff --git a/pkg/domain/infra/runtime_image_proxy.go b/pkg/domain/infra/runtime_image_proxy.go new file mode 100644 index 000000000..befc66b9a --- /dev/null +++ b/pkg/domain/infra/runtime_image_proxy.go @@ -0,0 +1,21 @@ +// +build ABISupport + +package infra + +import ( + "context" + + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" + "github.com/spf13/pflag" +) + +// ContainerEngine Image Proxy will be EOL'ed after podmanV2 is separated from libpod repo + +func NewLibpodImageRuntime(flags *pflag.FlagSet, opts entities.EngineOptions) (entities.ImageEngine, error) { + r, err := GetRuntime(context.Background(), flags, opts) + if err != nil { + return nil, err + } + return &abi.ImageEngine{Libpod: r}, nil +} diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go new file mode 100644 index 000000000..d59759707 --- /dev/null +++ b/pkg/domain/infra/runtime_libpod.go @@ -0,0 +1,328 @@ +package infra + +import ( + "context" + "fmt" + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/namespaces" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/storage" + "github.com/containers/storage/pkg/idtools" + "github.com/pkg/errors" + flag "github.com/spf13/pflag" +) + +type engineOpts struct { + name string + renumber bool + migrate bool + noStore bool + withFDS bool + flags entities.EngineOptions +} + +// GetRuntimeMigrate gets a libpod runtime that will perform a migration of existing containers +func GetRuntimeMigrate(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions, newRuntime string) (*libpod.Runtime, error) { + return getRuntime(ctx, fs, &engineOpts{ + name: newRuntime, + renumber: false, + migrate: true, + noStore: false, + withFDS: true, + flags: ef, + }) +} + +// GetRuntimeDisableFDs gets a libpod runtime that will disable sd notify +func GetRuntimeDisableFDs(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { + return getRuntime(ctx, fs, &engineOpts{ + renumber: false, + migrate: false, + noStore: false, + withFDS: false, + flags: ef, + }) +} + +// GetRuntimeRenumber gets a libpod runtime that will perform a lock renumber +func GetRuntimeRenumber(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { + return getRuntime(ctx, fs, &engineOpts{ + renumber: true, + migrate: false, + noStore: false, + withFDS: true, + flags: ef, + }) +} + +// GetRuntime generates a new libpod runtime configured by command line options +func GetRuntime(ctx context.Context, flags *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { + return getRuntime(ctx, flags, &engineOpts{ + renumber: false, + migrate: false, + noStore: false, + withFDS: true, + flags: ef, + }) +} + +// GetRuntimeNoStore generates a new libpod runtime configured by command line options +func GetRuntimeNoStore(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { + return getRuntime(ctx, fs, &engineOpts{ + renumber: false, + migrate: false, + noStore: true, + withFDS: true, + flags: ef, + }) +} + +func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpod.Runtime, error) { + options := []libpod.RuntimeOption{} + storageOpts := storage.StoreOptions{} + storageSet := false + + uidmapFlag := fs.Lookup("uidmap") + gidmapFlag := fs.Lookup("gidmap") + subuidname := fs.Lookup("subuidname") + subgidname := fs.Lookup("subgidname") + if (uidmapFlag != nil && gidmapFlag != nil && subuidname != nil && subgidname != nil) && + (uidmapFlag.Changed || gidmapFlag.Changed || subuidname.Changed || subgidname.Changed) { + userns, _ := fs.GetString("userns") + uidmapVal, _ := fs.GetStringSlice("uidmap") + gidmapVal, _ := fs.GetStringSlice("gidmap") + subuidVal, _ := fs.GetString("subuidname") + subgidVal, _ := fs.GetString("subgidname") + mappings, err := ParseIDMapping(namespaces.UsernsMode(userns), uidmapVal, gidmapVal, subuidVal, subgidVal) + if err != nil { + return nil, err + } + storageOpts.UIDMap = mappings.UIDMap + storageOpts.GIDMap = mappings.GIDMap + + storageSet = true + } + + if fs.Changed("root") { + storageSet = true + storageOpts.GraphRoot = opts.flags.Root + } + if fs.Changed("runroot") { + storageSet = true + storageOpts.RunRoot = opts.flags.Runroot + } + if len(storageOpts.RunRoot) > 50 { + return nil, errors.New("the specified runroot is longer than 50 characters") + } + if fs.Changed("storage-driver") { + storageSet = true + storageOpts.GraphDriverName = opts.flags.StorageDriver + // Overriding the default storage driver caused GraphDriverOptions from storage.conf to be ignored + storageOpts.GraphDriverOptions = []string{} + } + // This should always be checked after storage-driver is checked + if len(opts.flags.StorageOpts) > 0 { + storageSet = true + storageOpts.GraphDriverOptions = opts.flags.StorageOpts + } + if opts.migrate { + options = append(options, libpod.WithMigrate()) + if opts.name != "" { + options = append(options, libpod.WithMigrateRuntime(opts.name)) + } + } + + if opts.renumber { + options = append(options, libpod.WithRenumber()) + } + + // Only set this if the user changes storage config on the command line + if storageSet { + options = append(options, libpod.WithStorageConfig(storageOpts)) + } + + if !storageSet && opts.noStore { + options = append(options, libpod.WithNoStore()) + } + // TODO CLI flags for image config? + // TODO CLI flag for signature policy? + + if len(opts.flags.Namespace) > 0 { + options = append(options, libpod.WithNamespace(opts.flags.Namespace)) + } + + if fs.Changed("runtime") { + options = append(options, libpod.WithOCIRuntime(opts.flags.Runtime)) + } + + if fs.Changed("conmon") { + options = append(options, libpod.WithConmonPath(opts.flags.ConmonPath)) + } + if fs.Changed("tmpdir") { + options = append(options, libpod.WithTmpDir(opts.flags.TmpDir)) + } + if fs.Changed("network-cmd-path") { + options = append(options, libpod.WithNetworkCmdPath(opts.flags.NetworkCmdPath)) + } + + if fs.Changed("events-backend") { + options = append(options, libpod.WithEventsLogger(opts.flags.EventsBackend)) + } + + if fs.Changed("cgroup-manager") { + options = append(options, libpod.WithCgroupManager(opts.flags.CGroupManager)) + } else { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if rootless.IsRootless() && !unified { + options = append(options, libpod.WithCgroupManager("cgroupfs")) + } + } + + // TODO flag to set libpod static dir? + // TODO flag to set libpod tmp dir? + + if fs.Changed("cni-config-dir") { + options = append(options, libpod.WithCNIConfigDir(opts.flags.CniConfigDir)) + } + if fs.Changed("default-mounts-file") { + options = append(options, libpod.WithDefaultMountsFile(opts.flags.DefaultMountsFile)) + } + if fs.Changed("hooks-dir") { + options = append(options, libpod.WithHooksDir(opts.flags.HooksDir...)) + } + + // TODO flag to set CNI plugins dir? + + // TODO I don't think these belong here? + // Will follow up with a different PR to address + // + // Pod create options + + infraImageFlag := fs.Lookup("infra-image") + if infraImageFlag != nil && infraImageFlag.Changed { + infraImage, _ := fs.GetString("infra-image") + options = append(options, libpod.WithDefaultInfraImage(infraImage)) + } + + infraCommandFlag := fs.Lookup("infra-command") + if infraCommandFlag != nil && infraImageFlag.Changed { + infraCommand, _ := fs.GetString("infra-command") + options = append(options, libpod.WithDefaultInfraCommand(infraCommand)) + } + + if !opts.withFDS { + options = append(options, libpod.WithEnableSDNotify()) + } + return libpod.NewRuntime(ctx, options...) +} + +// ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping +func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*storage.IDMappingOptions, error) { + options := storage.IDMappingOptions{ + HostUIDMapping: true, + HostGIDMapping: true, + } + + if mode.IsKeepID() { + if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 { + return nil, errors.New("cannot specify custom mappings with --userns=keep-id") + } + if len(subUIDMap) > 0 || len(subGIDMap) > 0 { + return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id") + } + if rootless.IsRootless() { + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + uid := rootless.GetRootlessUID() + gid := rootless.GetRootlessGID() + + uids, gids, err := rootless.GetConfiguredMappings() + if err != nil { + return nil, errors.Wrapf(err, "cannot read mappings") + } + maxUID, maxGID := 0, 0 + for _, u := range uids { + maxUID += u.Size + } + for _, g := range gids { + maxGID += g.Size + } + + options.UIDMap, options.GIDMap = nil, nil + + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) + if maxUID > uid { + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) + } + + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) + if maxGID > gid { + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) + } + + options.HostUIDMapping = false + options.HostGIDMapping = false + } + // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op + return &options, nil + } + + if subGIDMap == "" && subUIDMap != "" { + subGIDMap = subUIDMap + } + if subUIDMap == "" && subGIDMap != "" { + subUIDMap = subGIDMap + } + if len(gidMapSlice) == 0 && len(uidMapSlice) != 0 { + gidMapSlice = uidMapSlice + } + if len(uidMapSlice) == 0 && len(gidMapSlice) != 0 { + uidMapSlice = gidMapSlice + } + if len(uidMapSlice) == 0 && subUIDMap == "" && os.Getuid() != 0 { + uidMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getuid())} + } + if len(gidMapSlice) == 0 && subGIDMap == "" && os.Getuid() != 0 { + gidMapSlice = []string{fmt.Sprintf("0:%d:1", os.Getgid())} + } + + if subUIDMap != "" && subGIDMap != "" { + mappings, err := idtools.NewIDMappings(subUIDMap, subGIDMap) + if err != nil { + return nil, err + } + options.UIDMap = mappings.UIDs() + options.GIDMap = mappings.GIDs() + } + parsedUIDMap, err := idtools.ParseIDMap(uidMapSlice, "UID") + if err != nil { + return nil, err + } + parsedGIDMap, err := idtools.ParseIDMap(gidMapSlice, "GID") + if err != nil { + return nil, err + } + options.UIDMap = append(options.UIDMap, parsedUIDMap...) + options.GIDMap = append(options.GIDMap, parsedGIDMap...) + if len(options.UIDMap) > 0 { + options.HostUIDMapping = false + } + if len(options.GIDMap) > 0 { + options.HostGIDMapping = false + } + return &options, nil +} diff --git a/pkg/domain/infra/runtime_proxy.go b/pkg/domain/infra/runtime_proxy.go new file mode 100644 index 000000000..2e38c74b9 --- /dev/null +++ b/pkg/domain/infra/runtime_proxy.go @@ -0,0 +1,21 @@ +// +build ABISupport + +package infra + +import ( + "context" + + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" + flag "github.com/spf13/pflag" +) + +// ContainerEngine Proxy will be EOL'ed after podmanV2 is separated from libpod repo + +func NewLibpodRuntime(flags *flag.FlagSet, opts entities.EngineOptions) (entities.ContainerEngine, error) { + r, err := GetRuntime(context.Background(), flags, opts) + if err != nil { + return nil, err + } + return &abi.ContainerEngine{Libpod: r}, nil +} diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go new file mode 100644 index 000000000..dc04b4e53 --- /dev/null +++ b/pkg/domain/infra/runtime_tunnel.go @@ -0,0 +1,35 @@ +// +build !ABISupport + +package infra + +import ( + "context" + "fmt" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/tunnel" +) + +func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, error) { + switch facts.EngineMode { + case entities.ABIMode: + return nil, fmt.Errorf("direct runtime not supported") + case entities.TunnelMode: + ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + return &tunnel.ContainerEngine{ClientCxt: ctx}, err + } + return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) +} + +// NewImageEngine factory provides a libpod runtime for image-related operations +func NewImageEngine(facts entities.EngineOptions) (entities.ImageEngine, error) { + switch facts.EngineMode { + case entities.ABIMode: + return nil, fmt.Errorf("direct image runtime not supported") + case entities.TunnelMode: + ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + return &tunnel.ImageEngine{ClientCxt: ctx}, err + } + return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go new file mode 100644 index 000000000..3db38ea5c --- /dev/null +++ b/pkg/domain/infra/tunnel/containers.go @@ -0,0 +1,174 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +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 +} + +func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, options entities.WaitOptions) ([]entities.WaitReport, error) { + var ( + responses []entities.WaitReport + ) + cons, err := getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range cons { + response := entities.WaitReport{Id: c.ID} + exitCode, err := containers.Wait(ic.ClientCxt, c.ID, &options.Condition) + if err != nil { + response.Error = err + } else { + response.ExitCode = exitCode + } + responses = append(responses, response) + } + return responses, nil +} + +func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + reports []*entities.PauseUnpauseReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := containers.Pause(ic.ClientCxt, c.ID) + reports = append(reports, &entities.PauseUnpauseReport{Id: c.ID, Err: err}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + reports []*entities.PauseUnpauseReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := containers.Unpause(ic.ClientCxt, c.ID) + reports = append(reports, &entities.PauseUnpauseReport{Id: c.ID, Err: err}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { + var ( + reports []*entities.StopReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + report := entities.StopReport{Id: c.ID} + report.Err = containers.Stop(ic.ClientCxt, c.ID, &options.Timeout) + // TODO we need to associate errors returned by http with common + // define.errors so that we can equity tests. this will allow output + // to be the same as the native client + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, options entities.KillOptions) ([]*entities.KillReport, error) { + var ( + reports []*entities.KillReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + reports = append(reports, &entities.KillReport{ + Id: c.ID, + Err: containers.Kill(ic.ClientCxt, c.ID, options.Signal), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { + var ( + reports []*entities.RestartReport + timeout *int + ) + if options.Timeout != nil { + t := int(*options.Timeout) + timeout = &t + } + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + reports = append(reports, &entities.RestartReport{ + Id: c.ID, + Err: containers.Restart(ic.ClientCxt, c.ID, timeout), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*entities.RmReport, error) { + var ( + reports []*entities.RmReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + // TODO there is no endpoint for container eviction. Need to discuss + for _, c := range ctrs { + reports = append(reports, &entities.RmReport{ + Id: c.ID, + Err: containers.Remove(ic.ClientCxt, c.ID, &options.Force, &options.Volumes), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.ContainerInspectOptions) ([]*entities.ContainerInspectReport, error) { + var ( + reports []*entities.ContainerInspectReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + for _, con := range ctrs { + data, err := containers.Inspect(ic.ClientCxt, con.ID, &options.Size) + if err != nil { + return nil, err + } + reports = append(reports, &entities.ContainerInspectReport{InspectContainerData: data}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerTop(ctx context.Context, options entities.TopOptions) (*entities.StringSliceReport, error) { + switch { + case options.Latest: + return nil, errors.New("latest is not supported") + case options.NameOrID == "": + return nil, errors.New("NameOrID must be specified") + } + + topOutput, err := containers.Top(ic.ClientCxt, options.NameOrID, options.Descriptors) + if err != nil { + return nil, err + } + return &entities.StringSliceReport{Value: topOutput}, nil +} diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go new file mode 100644 index 000000000..f9183c955 --- /dev/null +++ b/pkg/domain/infra/tunnel/helpers.go @@ -0,0 +1,76 @@ +package tunnel + +import ( + "context" + "strings" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/bindings/pods" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +func getContainersByContext(contextWithConnection context.Context, all bool, namesOrIds []string) ([]libpod.ListContainer, error) { + var ( + cons []libpod.ListContainer + ) + if all && len(namesOrIds) > 0 { + return nil, errors.New("cannot lookup containers and all") + } + c, err := containers.List(contextWithConnection, nil, &bindings.PTrue, nil, nil, nil, &bindings.PTrue) + if err != nil { + return nil, err + } + if all { + return c, err + } + for _, id := range namesOrIds { + var found bool + for _, con := range c { + if id == con.ID || strings.HasPrefix(con.ID, id) || util.StringInSlice(id, con.Names) { + cons = append(cons, con) + found = true + break + } + } + if !found { + return nil, errors.Errorf("unable to find container %q", id) + } + } + return cons, nil +} + +func getPodsByContext(contextWithConnection context.Context, all bool, namesOrIds []string) ([]*entities.ListPodsReport, error) { + var ( + sPods []*entities.ListPodsReport + ) + if all && len(namesOrIds) > 0 { + return nil, errors.New("cannot lookup specific pods and all") + } + + fPods, err := pods.List(contextWithConnection, nil) + if err != nil { + return nil, err + } + if all { + return fPods, nil + } + for _, nameOrId := range namesOrIds { + var found bool + for _, f := range fPods { + if f.Name == nameOrId || strings.HasPrefix(f.Id, nameOrId) { + sPods = append(sPods, f) + found = true + break + } + } + if !found { + return nil, errors.Wrapf(define.ErrNoSuchPod, "unable to find pod %q", nameOrId) + } + } + return sPods, nil +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go new file mode 100644 index 000000000..6a3adc9ee --- /dev/null +++ b/pkg/domain/infra/tunnel/images.go @@ -0,0 +1,87 @@ +package tunnel + +import ( + "context" + + images "github.com/containers/libpod/pkg/bindings/images" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/utils" +) + +func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { + found, err := images.Exists(ir.ClientCxt, nameOrId) + return &entities.BoolReport{Value: found}, err +} + +func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { + report := entities.ImageDeleteReport{} + + for _, id := range nameOrId { + results, err := images.Remove(ir.ClientCxt, id, &opts.Force) + if err != nil { + return nil, err + } + for _, e := range results { + if a, ok := e["Deleted"]; ok { + report.Deleted = append(report.Deleted, a) + } + + if a, ok := e["Untagged"]; ok { + report.Untagged = append(report.Untagged, a) + } + } + } + return &report, nil +} + +func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) ([]*entities.ImageSummary, error) { + images, err := images.List(ir.ClientCxt, &opts.All, opts.Filters) + + if err != nil { + return nil, err + } + + is := make([]*entities.ImageSummary, len(images)) + for i, img := range images { + hold := entities.ImageSummary{} + if err := utils.DeepCopy(&hold, img); err != nil { + return nil, err + } + is[i] = &hold + } + return is, nil +} + +func (ir *ImageEngine) History(ctx context.Context, nameOrId string, opts entities.ImageHistoryOptions) (*entities.ImageHistoryReport, error) { + results, err := images.History(ir.ClientCxt, nameOrId) + if err != nil { + return nil, err + } + + history := entities.ImageHistoryReport{ + Layers: make([]entities.ImageHistoryLayer, len(results)), + } + + for i, layer := range results { + hold := entities.ImageHistoryLayer{} + _ = utils.DeepCopy(&hold, layer) + history.Layers[i] = hold + } + return &history, nil +} + +func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { + results, err := images.Prune(ir.ClientCxt, &opts.All, opts.Filters) + if err != nil { + return nil, err + } + + report := entities.ImagePruneReport{ + Report: entities.Report{ + Id: results, + Err: nil, + }, + Size: 0, + } + return &report, nil +} diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go new file mode 100644 index 000000000..4894874e5 --- /dev/null +++ b/pkg/domain/infra/tunnel/pods.go @@ -0,0 +1,179 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/pods" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" +) + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + exists, err := pods.Exists(ic.ClientCxt, nameOrId) + return &entities.BoolReport{Value: exists}, err +} + +func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, options entities.PodKillOptions) ([]*entities.PodKillReport, error) { + var ( + reports []*entities.PodKillReport + ) + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Kill(ic.ClientCxt, p.Id, &options.Signal) + if err != nil { + report := entities.PodKillReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) { + var ( + reports []*entities.PodPauseReport + ) + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Pause(ic.ClientCxt, p.Id) + if err != nil { + report := entities.PodPauseReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodUnpause(ctx context.Context, namesOrIds []string, options entities.PodunpauseOptions) ([]*entities.PodUnpauseReport, error) { + var ( + reports []*entities.PodUnpauseReport + ) + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Unpause(ic.ClientCxt, p.Id) + if err != nil { + report := entities.PodUnpauseReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodStop(ctx context.Context, namesOrIds []string, options entities.PodStopOptions) ([]*entities.PodStopReport, error) { + var ( + reports []*entities.PodStopReport + timeout int = -1 + ) + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + if options.Timeout != -1 { + timeout = options.Timeout + } + for _, p := range foundPods { + response, err := pods.Stop(ic.ClientCxt, p.Id, &timeout) + if err != nil { + report := entities.PodStopReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodRestart(ctx context.Context, namesOrIds []string, options entities.PodRestartOptions) ([]*entities.PodRestartReport, error) { + var reports []*entities.PodRestartReport + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Restart(ic.ClientCxt, p.Id) + if err != nil { + report := entities.PodRestartReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodStart(ctx context.Context, namesOrIds []string, options entities.PodStartOptions) ([]*entities.PodStartReport, error) { + var reports []*entities.PodStartReport + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Start(ic.ClientCxt, p.Id) + if err != nil { + report := entities.PodStartReport{ + Errs: []error{err}, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, options entities.PodRmOptions) ([]*entities.PodRmReport, error) { + var reports []*entities.PodRmReport + foundPods, err := getPodsByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, p := range foundPods { + response, err := pods.Remove(ic.ClientCxt, p.Id, &options.Force) + if err != nil { + report := entities.PodRmReport{ + Err: err, + Id: p.Id, + } + reports = append(reports, &report) + continue + } + reports = append(reports, response) + } + return reports, nil +} + +func (ic *ContainerEngine) PodCreate(ctx context.Context, opts entities.PodCreateOptions) (*entities.PodCreateReport, error) { + podSpec := specgen.NewPodSpecGenerator() + opts.ToPodSpecGen(podSpec) + return pods.CreatePodFromSpec(ic.ClientCxt, podSpec) +} diff --git a/pkg/domain/infra/tunnel/runtime.go b/pkg/domain/infra/tunnel/runtime.go new file mode 100644 index 000000000..c111f99e9 --- /dev/null +++ b/pkg/domain/infra/tunnel/runtime.go @@ -0,0 +1,15 @@ +package tunnel + +import ( + "context" +) + +// Image-related runtime using an ssh-tunnel to utilize Podman service +type ImageEngine struct { + ClientCxt context.Context +} + +// Container-related runtime using an ssh-tunnel to utilize Podman service +type ContainerEngine struct { + ClientCxt context.Context +} diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go new file mode 100644 index 000000000..e48a7fa7c --- /dev/null +++ b/pkg/domain/infra/tunnel/volumes.go @@ -0,0 +1,70 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/volumes" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) { + response, err := volumes.Create(ic.ClientCxt, opts) + if err != nil { + return nil, err + } + return &entities.IdOrNameResponse{IdOrName: response.Name}, nil +} + +func (ic *ContainerEngine) VolumeRm(ctx context.Context, namesOrIds []string, opts entities.VolumeRmOptions) ([]*entities.VolumeRmReport, error) { + var ( + reports []*entities.VolumeRmReport + ) + + if opts.All { + vols, err := volumes.List(ic.ClientCxt, nil) + if err != nil { + return nil, err + } + for _, v := range vols { + namesOrIds = append(namesOrIds, v.Name) + } + } + for _, id := range namesOrIds { + reports = append(reports, &entities.VolumeRmReport{ + Err: volumes.Remove(ic.ClientCxt, id, &opts.Force), + Id: id, + }) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []string, opts entities.VolumeInspectOptions) ([]*entities.VolumeInspectReport, error) { + var ( + reports []*entities.VolumeInspectReport + ) + if opts.All { + vols, err := volumes.List(ic.ClientCxt, nil) + if err != nil { + return nil, err + } + for _, v := range vols { + namesOrIds = append(namesOrIds, v.Name) + } + } + for _, id := range namesOrIds { + data, err := volumes.Inspect(ic.ClientCxt, id) + if err != nil { + return nil, err + } + reports = append(reports, &entities.VolumeInspectReport{VolumeConfigResponse: data}) + } + return reports, nil +} + +func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { + return volumes.Prune(ic.ClientCxt) +} + +func (ic *ContainerEngine) VolumeList(ctx context.Context, opts entities.VolumeListOptions) ([]*entities.VolumeListReport, error) { + return volumes.List(ic.ClientCxt, opts.Filter) +} diff --git a/pkg/domain/utils/utils.go b/pkg/domain/utils/utils.go new file mode 100644 index 000000000..c17769f62 --- /dev/null +++ b/pkg/domain/utils/utils.go @@ -0,0 +1,41 @@ +package utils + +import ( + "net/url" + "strings" + + jsoniter "github.com/json-iterator/go" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// DeepCopy does a deep copy of a structure +// Error checking of parameters delegated to json engine +var DeepCopy = func(dst interface{}, src interface{}) error { + payload, err := json.Marshal(src) + if err != nil { + return err + } + + err = json.Unmarshal(payload, dst) + if err != nil { + return err + } + return nil +} + +func ToLibpodFilters(f url.Values) (filters []string) { + for k, v := range f { + filters = append(filters, k+"="+v[0]) + } + return +} + +func ToUrlValues(f []string) (filters url.Values) { + filters = make(url.Values) + for _, v := range f { + t := strings.SplitN(v, "=", 2) + filters.Add(t[0], t[1]) + } + return +} diff --git a/pkg/env/env.go b/pkg/env/env.go new file mode 100644 index 000000000..c6a1a0d28 --- /dev/null +++ b/pkg/env/env.go @@ -0,0 +1,126 @@ +// Package for processing environment variables. +package env + +// TODO: we need to add tests for this package. + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/pkg/errors" +) + +// DefaultEnvVariables sets $PATH and $TERM. +var DefaultEnvVariables = map[string]string{ + "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM": "xterm", +} + +const whiteSpaces = " \t" + +// ParseSlice parses the specified slice and transforms it into an environment +// map. +func ParseSlice(s []string) (map[string]string, error) { + env := make(map[string]string, len(s)) + for _, e := range s { + if err := parseEnv(env, e); err != nil { + return nil, err + } + } + return env, nil +} + +// Slice transforms the specified map of environment variables into a +// slice. If a value is non-empty, the key and value are joined with '='. +func Slice(m map[string]string) []string { + env := make([]string, len(m)) + for k, v := range m { + var s string + if len(v) > 0 { + s = fmt.Sprintf("%s=%s", k, v) + } else { + s = k + } + env = append(env, s) + } + return env +} + +// Join joins the two environment maps with override overriding base. +func Join(base map[string]string, override map[string]string) map[string]string { + if len(base) == 0 { + return override + } + for k, v := range override { + base[k] = v + } + return base +} + +// ParseFile parses the specified path for environment variables and returns them +// as a map. +func ParseFile(path string) (_ map[string]string, err error) { + env := make(map[string]string) + defer func() { + if err != nil { + err = errors.Wrapf(err, "error parsing env file %q", path) + } + }() + + fh, err := os.Open(path) + if err != nil { + return nil, err + } + defer fh.Close() + + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + // trim the line from all leading whitespace first + line := strings.TrimLeft(scanner.Text(), whiteSpaces) + // line is not empty, and not starting with '#' + if len(line) > 0 && !strings.HasPrefix(line, "#") { + if err := parseEnv(env, line); err != nil { + return nil, err + } + } + } + return env, scanner.Err() +} + +func parseEnv(env map[string]string, line string) error { + data := strings.SplitN(line, "=", 2) + + // catch invalid variables such as "=" or "=A" + if data[0] == "" { + return errors.Errorf("invalid environment variable: %q", line) + } + + // trim the front of a variable, but nothing else + name := strings.TrimLeft(data[0], whiteSpaces) + if strings.ContainsAny(name, whiteSpaces) { + return errors.Errorf("name %q has white spaces, poorly formatted name", name) + } + + if len(data) > 1 { + env[name] = data[1] + } else { + if strings.HasSuffix(name, "*") { + name = strings.TrimSuffix(name, "*") + for _, e := range os.Environ() { + part := strings.SplitN(e, "=", 2) + if len(part) < 2 { + continue + } + if strings.HasPrefix(part[0], name) { + env[part[0]] = part[1] + } + } + } else if val, ok := os.LookupEnv(name); ok { + // if only a pass-through variable is given, clean it up. + env[name] = val + } + } + return nil +} diff --git a/pkg/hooks/exec/exec.go b/pkg/hooks/exec/exec.go index 4038e3d94..77b350573 100644 --- a/pkg/hooks/exec/exec.go +++ b/pkg/hooks/exec/exec.go @@ -5,13 +5,13 @@ import ( "bytes" "context" "fmt" - "github.com/sirupsen/logrus" "io" osexec "os/exec" "time" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // DefaultPostKillTimeout is the recommended default post-kill timeout. diff --git a/pkg/inspect/inspect.go b/pkg/inspect/inspect.go index 569f208d9..b04ce71a5 100644 --- a/pkg/inspect/inspect.go +++ b/pkg/inspect/inspect.go @@ -6,7 +6,7 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod/driver" "github.com/opencontainers/go-digest" - "github.com/opencontainers/image-spec/specs-go/v1" + v1 "github.com/opencontainers/image-spec/specs-go/v1" ) // ImageData holds the inspect information of an image diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go index a249dd753..dff25f74f 100644 --- a/pkg/lookup/lookup.go +++ b/pkg/lookup/lookup.go @@ -4,7 +4,7 @@ import ( "os" "strconv" - "github.com/cyphar/filepath-securejoin" + securejoin "github.com/cyphar/filepath-securejoin" "github.com/opencontainers/runc/libcontainer/user" "github.com/sirupsen/logrus" ) diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 78b55bb2a..14453e7f4 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -101,7 +101,7 @@ func (n UsernsMode) IsPrivate() bool { func (n UsernsMode) Valid() bool { parts := strings.Split(string(n), ":") switch mode := parts[0]; mode { - case "", hostType, "keep-id", nsType: + case "", privateType, hostType, "keep-id", nsType: case containerType: if len(parts) != 2 || parts[1] == "" { return false @@ -173,7 +173,7 @@ func (n UTSMode) Container() string { func (n UTSMode) Valid() bool { parts := strings.Split(string(n), ":") switch mode := parts[0]; mode { - case "", hostType: + case "", privateType, hostType: case containerType: if len(parts) != 2 || parts[1] == "" { return false @@ -255,7 +255,7 @@ func (n PidMode) IsContainer() bool { func (n PidMode) Valid() bool { parts := strings.Split(string(n), ":") switch mode := parts[0]; mode { - case "", hostType: + case "", privateType, hostType: case containerType: if len(parts) != 2 || parts[1] == "" { return false diff --git a/pkg/network/netconflist.go b/pkg/network/netconflist.go index a8217097a..34ff00024 100644 --- a/pkg/network/netconflist.go +++ b/pkg/network/netconflist.go @@ -110,7 +110,6 @@ func NewPortMapPlugin() PortMapConfig { func NewFirewallPlugin() FirewallConfig { return FirewallConfig{ PluginType: "firewall", - Backend: "iptables", } } diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index e85bcb377..20618e2dc 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -10,7 +10,7 @@ import ( "sync" "github.com/containers/libpod/pkg/resolvconf/dns" - "github.com/docker/docker/pkg/ioutils" + "github.com/containers/storage/pkg/ioutils" "github.com/sirupsen/logrus" ) diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index db898e706..6643bfbbf 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -108,10 +108,9 @@ do_pause () } static char ** -get_cmd_line_args (pid_t pid) +get_cmd_line_args () { int fd; - char path[PATH_MAX]; char *buffer; size_t allocated; size_t used = 0; @@ -119,11 +118,7 @@ get_cmd_line_args (pid_t pid) int i, argc = 0; char **argv; - if (pid) - sprintf (path, "/proc/%d/cmdline", pid); - else - strcpy (path, "/proc/self/cmdline"); - fd = open (path, O_RDONLY); + fd = open ("/proc/self/cmdline", O_RDONLY); if (fd < 0) return NULL; @@ -196,7 +191,7 @@ can_use_shortcut () return false; #endif - argv = get_cmd_line_args (0); + argv = get_cmd_line_args (); if (argv == NULL) return false; @@ -542,7 +537,6 @@ create_pause_process (const char *pause_pid_file_path, char **argv) int reexec_userns_join (int userns, int mountns, char *pause_pid_file_path) { - pid_t ppid = getpid (); char uid[16]; char gid[16]; char **argv; @@ -559,7 +553,7 @@ reexec_userns_join (int userns, int mountns, char *pause_pid_file_path) sprintf (uid, "%d", geteuid ()); sprintf (gid, "%d", getegid ()); - argv = get_cmd_line_args (ppid); + argv = get_cmd_line_args (); if (argv == NULL) { fprintf (stderr, "cannot read argv: %s\n", strerror (errno)); @@ -724,7 +718,6 @@ reexec_in_user_namespace (int ready, char *pause_pid_file_path, char *file_to_re int ret; pid_t pid; char b; - pid_t ppid = getpid (); char **argv; char uid[16]; char gid[16]; @@ -801,7 +794,7 @@ reexec_in_user_namespace (int ready, char *pause_pid_file_path, char *file_to_re _exit (EXIT_FAILURE); } - argv = get_cmd_line_args (ppid); + argv = get_cmd_line_args (); if (argv == NULL) { fprintf (stderr, "cannot read argv: %s\n", strerror (errno)); diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 2b51f4e09..1c1ed39df 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -19,7 +19,7 @@ import ( "io/ioutil" "os" "os/exec" - "syscall" + "os/signal" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/storage/pkg/reexec" @@ -29,6 +29,7 @@ import ( rkbuiltin "github.com/rootless-containers/rootlesskit/pkg/port/builtin" rkportutil "github.com/rootless-containers/rootlesskit/pkg/port/portutil" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) const ( @@ -101,6 +102,28 @@ func parent() error { return err } + sigC := make(chan os.Signal, 1) + signal.Notify(sigC, unix.SIGPIPE) + defer func() { + // dummy signal to terminate the goroutine + sigC <- unix.SIGKILL + }() + go func() { + defer func() { + signal.Stop(sigC) + close(sigC) + }() + + s := <-sigC + if s == unix.SIGPIPE { + if f, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755); err == nil { + unix.Dup2(int(f.Fd()), 1) // nolint:errcheck + unix.Dup2(int(f.Fd()), 2) // nolint:errcheck + f.Close() + } + } + }() + // create the parent driver stateDir, err := ioutil.TempDir(cfg.TmpDir, "rootlessport") if err != nil { @@ -160,8 +183,15 @@ func parent() error { return err } + childErrCh := make(chan error) + go func() { + err := cmd.Wait() + childErrCh <- err + close(childErrCh) + }() + defer func() { - if err := syscall.Kill(cmd.Process.Pid, syscall.SIGTERM); err != nil { + if err := unix.Kill(cmd.Process.Pid, unix.SIGTERM); err != nil { logrus.WithError(err).Warn("kill child process") } }() @@ -174,6 +204,10 @@ outer: case <-initComplete: logrus.Infof("initComplete is closed; parent and child established the communication channel") break outer + case err := <-childErrCh: + if err != nil { + return err + } case err := <-errCh: if err != nil { return err diff --git a/pkg/signal/signal_linux.go b/pkg/signal/signal_linux.go index 3d549898f..76ab16ec7 100644 --- a/pkg/signal/signal_linux.go +++ b/pkg/signal/signal_linux.go @@ -104,11 +104,11 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { } return syscall.Signal(s), nil } - signal, ok := signalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] + sig, ok := signalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] if !ok { return -1, fmt.Errorf("invalid signal: %s", rawSignal) } - return signal, nil + return sig, nil } // CatchAll catches all signals and relays them to the specified channel. @@ -125,3 +125,18 @@ func StopCatch(sigc chan os.Signal) { signal.Stop(sigc) close(sigc) } + +// ParseSignalNameOrNumber translates a string to a valid syscall signal. Input +// can be a name or number representation i.e. "KILL" "9" +func ParseSignalNameOrNumber(rawSignal string) (syscall.Signal, error) { + s, err := ParseSignal(rawSignal) + if err == nil { + return s, nil + } + for k, v := range signalMap { + if k == strings.ToUpper(rawSignal) { + return v, nil + } + } + return -1, fmt.Errorf("invalid signal: %s", rawSignal) +} diff --git a/pkg/signal/signal_unsupported.go b/pkg/signal/signal_unsupported.go index 0a92a5b3a..f946d802d 100644 --- a/pkg/signal/signal_unsupported.go +++ b/pkg/signal/signal_unsupported.go @@ -26,3 +26,9 @@ func CatchAll(sigc chan os.Signal) { func StopCatch(sigc chan os.Signal) { panic("Unsupported on non-linux platforms") } + +// ParseSignalNameOrNumber translates a string to a valid syscall signal. Input +// can be a name or number representation i.e. "KILL" "9" +func ParseSignalNameOrNumber(rawSignal string) (syscall.Signal, error) { + return 0, fmt.Errorf("unsupported on non-linux platforms") +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 02678a687..daa997104 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -112,6 +112,7 @@ type NetworkConfig struct { type SecurityConfig struct { CapAdd []string // cap-add CapDrop []string // cap-drop + CapRequired []string // cap-required LabelOpts []string //SecurityOpts NoNewPrivs bool //SecurityOpts ApparmorProfile string //SecurityOpts @@ -143,6 +144,7 @@ type CreateConfig struct { InitPath string //init-path Image string ImageID string + RawImageName string BuiltinImgVolumes map[string]struct{} // volumes defined in the image config ImageVolumeType string // how to handle the image volume, either bind, tmpfs, or ignore Interactive bool //interactive @@ -156,6 +158,7 @@ type CreateConfig struct { Resources CreateResourceConfig RestartPolicy string Rm bool //rm + Rmi bool //rmi StopSignal syscall.Signal // stop-signal StopTimeout uint // stop-timeout Systemd bool @@ -193,6 +196,7 @@ func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, err if err != nil { return nil, err } + storageConfig := runtime.StorageConfig() // We need a cleanup process for containers in the current model. // But we can't assume that the caller is Podman - it could be another @@ -205,23 +209,23 @@ func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, err } command := []string{cmd, - "--root", config.StorageConfig.GraphRoot, - "--runroot", config.StorageConfig.RunRoot, + "--root", storageConfig.GraphRoot, + "--runroot", storageConfig.RunRoot, "--log-level", logrus.GetLevel().String(), - "--cgroup-manager", config.CgroupManager, - "--tmpdir", config.TmpDir, + "--cgroup-manager", config.Engine.CgroupManager, + "--tmpdir", config.Engine.TmpDir, } - if config.OCIRuntime != "" { - command = append(command, []string{"--runtime", config.OCIRuntime}...) + if config.Engine.OCIRuntime != "" { + command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) } - if config.StorageConfig.GraphDriverName != "" { - command = append(command, []string{"--storage-driver", config.StorageConfig.GraphDriverName}...) + if storageConfig.GraphDriverName != "" { + command = append(command, []string{"--storage-driver", storageConfig.GraphDriverName}...) } - for _, opt := range config.StorageConfig.GraphDriverOptions { + for _, opt := range storageConfig.GraphDriverOptions { command = append(command, []string{"--storage-opt", opt}...) } - if config.EventsLogger != "" { - command = append(command, []string{"--events-backend", config.EventsLogger}...) + if config.Engine.EventsLogger != "" { + command = append(command, []string{"--events-backend", config.Engine.EventsLogger}...) } if c.Syslog { @@ -233,6 +237,10 @@ func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, err command = append(command, "--rm") } + if c.Rmi { + command = append(command, "--rmi") + } + return command, nil } @@ -342,7 +350,7 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l options = append(options, nsOpts...) // Gather up the options for NewContainer which consist of With... funcs - options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image)) + options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image, c.RawImageName)) options = append(options, libpod.WithConmonPidFile(c.ConmonPidFile)) options = append(options, libpod.WithLabels(c.Labels)) options = append(options, libpod.WithShmSize(c.Resources.ShmSize)) diff --git a/pkg/spec/security.go b/pkg/spec/security.go index 3bad9f97a..0f8d36f00 100644 --- a/pkg/spec/security.go +++ b/pkg/spec/security.go @@ -4,11 +4,13 @@ import ( "fmt" "strings" + "github.com/containers/common/pkg/capabilities" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/capabilities" + "github.com/containers/libpod/pkg/util" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // ToCreateOptions convert the SecurityConfig to a slice of container create @@ -113,28 +115,49 @@ func (c *SecurityConfig) ConfigureGenerator(g *generate.Generator, user *UserCon configSpec := g.Config var err error - var caplist []string + var defaultCaplist []string bounding := configSpec.Process.Capabilities.Bounding if useNotRoot(user.User) { - configSpec.Process.Capabilities.Bounding = caplist + configSpec.Process.Capabilities.Bounding = defaultCaplist } - caplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, c.CapAdd, c.CapDrop) + defaultCaplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, c.CapAdd, c.CapDrop) if err != nil { return err } - configSpec.Process.Capabilities.Bounding = caplist - configSpec.Process.Capabilities.Permitted = caplist - configSpec.Process.Capabilities.Inheritable = caplist - configSpec.Process.Capabilities.Effective = caplist - configSpec.Process.Capabilities.Ambient = caplist + privCapRequired := []string{} + + if !c.Privileged && len(c.CapRequired) > 0 { + // Pass CapRequired in CapAdd field to normalize capabilities names + capRequired, err := capabilities.MergeCapabilities(nil, c.CapRequired, nil) + if err != nil { + logrus.Errorf("capabilities requested by user or image are not valid: %q", strings.Join(c.CapRequired, ",")) + } else { + // Verify all capRequiered are in the defaultCapList + for _, cap := range capRequired { + if !util.StringInSlice(cap, defaultCaplist) { + privCapRequired = append(privCapRequired, cap) + } + } + } + if len(privCapRequired) == 0 { + defaultCaplist = capRequired + } else { + logrus.Errorf("capabilities requested by user or image are not allowed by default: %q", strings.Join(privCapRequired, ",")) + } + } + configSpec.Process.Capabilities.Bounding = defaultCaplist + configSpec.Process.Capabilities.Permitted = defaultCaplist + configSpec.Process.Capabilities.Inheritable = defaultCaplist + configSpec.Process.Capabilities.Effective = defaultCaplist + configSpec.Process.Capabilities.Ambient = defaultCaplist if useNotRoot(user.User) { - caplist, err = capabilities.MergeCapabilities(bounding, c.CapAdd, c.CapDrop) + defaultCaplist, err = capabilities.MergeCapabilities(bounding, c.CapAdd, c.CapDrop) if err != nil { return err } } - configSpec.Process.Capabilities.Bounding = caplist + configSpec.Process.Capabilities.Bounding = defaultCaplist // HANDLE SECCOMP if c.SeccompProfilePath != "unconfined" { diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index a4ae22efd..d4fd5976f 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -3,12 +3,14 @@ package createconfig import ( "strings" + "github.com/containers/common/pkg/capabilities" + cconfig "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" - libpodconfig "github.com/containers/libpod/libpod/config" - "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/sysinfo" + "github.com/containers/libpod/pkg/util" "github.com/docker/go-units" "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -78,6 +80,37 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM g.AddLinuxMaskedPaths("/sys/kernel") } } + var runtimeConfig *cconfig.Config + + if runtime != nil { + runtimeConfig, err = runtime.GetConfig() + if err != nil { + return nil, err + } + g.Config.Process.Capabilities.Bounding = runtimeConfig.Containers.DefaultCapabilities + sysctls, err := util.ValidateSysctls(runtimeConfig.Containers.DefaultSysctls) + if err != nil { + return nil, err + } + + for name, val := range config.Security.Sysctl { + sysctls[name] = val + } + config.Security.Sysctl = sysctls + if !util.StringInSlice("host", config.Resources.Ulimit) { + config.Resources.Ulimit = append(runtimeConfig.Containers.DefaultUlimits, config.Resources.Ulimit...) + } + if config.Resources.PidsLimit < 0 && !config.cgroupDisabled() { + config.Resources.PidsLimit = runtimeConfig.Containers.PidsLimit + } + + } else { + g.Config.Process.Capabilities.Bounding = cconfig.DefaultCapabilities + if config.Resources.PidsLimit < 0 && !config.cgroupDisabled() { + config.Resources.PidsLimit = cconfig.DefaultPidsLimit + } + } + gid5Available := true if isRootless { nGids, err := GetAvailableGids() @@ -150,7 +183,6 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM for key, val := range config.Annotations { g.AddAnnotation(key, val) } - g.AddProcessEnv("container", "podman") addedResources := false @@ -240,16 +272,6 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } } - // SECURITY OPTS - var runtimeConfig *libpodconfig.Config - - if runtime != nil { - runtimeConfig, err = runtime.GetConfig() - if err != nil { - return nil, err - } - } - g.SetProcessNoNewPrivileges(config.Security.NoNewPrivs) if !config.Security.Privileged { @@ -259,7 +281,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM // Unless already set via the CLI, check if we need to disable process // labels or set the defaults. if len(config.Security.LabelOpts) == 0 && runtimeConfig != nil { - if !runtimeConfig.EnableLabeling { + if !runtimeConfig.Containers.EnableLabeling { // Disabled in the config. config.Security.LabelOpts = append(config.Security.LabelOpts, "disable") } else if err := config.Security.SetLabelOpts(runtime, &config.Pid, &config.Ipc); err != nil { @@ -282,7 +304,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM if err != nil { return nil, err } - if (!cgroup2 || (runtimeConfig != nil && runtimeConfig.CgroupManager != define.SystemdCgroupsManager)) && config.Resources.PidsLimit == sysinfo.GetDefaultPidsLimit() { + if (!cgroup2 || (runtimeConfig != nil && runtimeConfig.Engine.CgroupManager != cconfig.SystemdCgroupsManager)) && config.Resources.PidsLimit == sysinfo.GetDefaultPidsLimit() { setPidLimit = false } } @@ -292,6 +314,9 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } } + // Make sure to always set the default variables unless overridden in the + // config. + config.Env = env.Join(env.DefaultEnvVariables, config.Env) for name, val := range config.Env { g.AddProcessEnv(name, val) } @@ -327,6 +352,18 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } configSpec := g.Config + // If the container image specifies an label with a + // capabilities.ContainerImageLabel then split the comma separated list + // of capabilities and record them. This list indicates the only + // capabilities, required to run the container. + var capRequired []string + for key, val := range config.Labels { + if util.StringInSlice(key, capabilities.ContainerImageLabels) { + capRequired = strings.Split(val, ",") + } + } + config.Security.CapRequired = capRequired + if err := config.Security.ConfigureGenerator(&g, &config.User); err != nil { return nil, err } @@ -359,7 +396,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM configSpec.Linux.Resources = &spec.LinuxResources{} } - canUseResources := cgroup2 && runtimeConfig != nil && (runtimeConfig.CgroupManager == define.SystemdCgroupsManager) + canUseResources := cgroup2 && runtimeConfig != nil && (runtimeConfig.Engine.CgroupManager == cconfig.SystemdCgroupsManager) if addedResources && !canUseResources { return nil, errors.New("invalid configuration, cannot specify resource limits without cgroups v2 and --cgroup-manager=systemd") @@ -416,6 +453,10 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM return configSpec, nil } +func (config *CreateConfig) cgroupDisabled() bool { + return config.Cgroup.Cgroups == "disabled" +} + func BlockAccessToKernelFilesystems(privileged, pidModeIsHost bool, g *generate.Generator) { if !privileged { for _, mp := range []string{ diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index c365701de..b0687b4c2 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -124,7 +124,7 @@ func (config *CreateConfig) parseVolumes(runtime *libpod.Runtime) ([]spec.Mount, if err != nil { return nil, nil, err } - initPath = rtc.InitPath + initPath = rtc.Engine.InitPath } initMount, err := config.addContainerInitBinary(initPath) if err != nil { diff --git a/pkg/specgen/config_unsupported.go b/pkg/specgen/config_unsupported.go index 5d24ac39c..c2d3257c9 100644 --- a/pkg/specgen/config_unsupported.go +++ b/pkg/specgen/config_unsupported.go @@ -3,10 +3,11 @@ package specgen import ( + "github.com/containers/libpod/libpod/image" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) -func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { +func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { return nil, errors.New("function not supported on non-linux OS's") } diff --git a/pkg/specgen/create.go b/pkg/specgen/container_create.go index 34f9ffac2..b4039bb91 100644 --- a/pkg/specgen/create.go +++ b/pkg/specgen/container_create.go @@ -2,18 +2,19 @@ package specgen import ( "context" + "os" + + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/config" "github.com/containers/libpod/libpod/define" + "github.com/containers/storage" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "os" ) // MakeContainer creates a container based on the SpecGenerator func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, error) { - var pod *libpod.Pod - if err := s.validate(rt); err != nil { + if err := s.validate(); err != nil { return nil, errors.Wrap(err, "invalid config provided") } rtc, err := rt.GetConfig() @@ -21,7 +22,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er return nil, err } - options, err := s.createContainerOptions(rt, pod) + options, err := s.createContainerOptions(rt) if err != nil { return nil, err } @@ -30,13 +31,13 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er if err != nil { return nil, err } - options = append(options, s.createExitCommandOption(rtc, podmanPath)) + options = append(options, s.createExitCommandOption(rt.StorageConfig(), rtc, podmanPath)) newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) if err != nil { return nil, err } - options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image)) + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) runtimeSpec, err := s.toOCISpec(rt, newImage) if err != nil { @@ -45,7 +46,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er return rt.NewContainer(context.Background(), runtimeSpec, options...) } -func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) { +func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -60,6 +61,10 @@ func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime, pod *libpod.P options = append(options, libpod.WithName(s.Name)) } if s.Pod != "" { + pod, err := rt.LookupPod(s.Pod) + if err != nil { + return nil, err + } logrus.Debugf("adding container to pod %s", s.Pod) options = append(options, rt.WithPod(pod)) } @@ -115,7 +120,6 @@ func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime, pod *libpod.P } options = append(options, namespaceOptions...) - // TODO NetworkNS still needs to be done! if len(s.ConmonPidFile) > 0 { options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile)) } @@ -145,7 +149,7 @@ func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime, pod *libpod.P return options, nil } -func (s *SpecGenerator) createExitCommandOption(config *config.Config, podmanPath string) libpod.CtrCreateOption { +func (s *SpecGenerator) createExitCommandOption(storageConfig storage.StoreOptions, config *config.Config, podmanPath string) libpod.CtrCreateOption { // We need a cleanup process for containers in the current model. // But we can't assume that the caller is Podman - it could be another // user of the API. @@ -153,23 +157,23 @@ func (s *SpecGenerator) createExitCommandOption(config *config.Config, podmanPat // still invoke a cleanup process. command := []string{podmanPath, - "--root", config.StorageConfig.GraphRoot, - "--runroot", config.StorageConfig.RunRoot, + "--root", storageConfig.GraphRoot, + "--runroot", storageConfig.RunRoot, "--log-level", logrus.GetLevel().String(), - "--cgroup-manager", config.CgroupManager, - "--tmpdir", config.TmpDir, + "--cgroup-manager", config.Engine.CgroupManager, + "--tmpdir", config.Engine.TmpDir, } - if config.OCIRuntime != "" { - command = append(command, []string{"--runtime", config.OCIRuntime}...) + if config.Engine.OCIRuntime != "" { + command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) } - if config.StorageConfig.GraphDriverName != "" { - command = append(command, []string{"--storage-driver", config.StorageConfig.GraphDriverName}...) + if storageConfig.GraphDriverName != "" { + command = append(command, []string{"--storage-driver", storageConfig.GraphDriverName}...) } - for _, opt := range config.StorageConfig.GraphDriverOptions { + for _, opt := range storageConfig.GraphDriverOptions { command = append(command, []string{"--storage-opt", opt}...) } - if config.EventsLogger != "" { - command = append(command, []string{"--events-backend", config.EventsLogger}...) + if config.Engine.EventsLogger != "" { + command = append(command, []string{"--events-backend", config.Engine.EventsLogger}...) } // TODO Mheon wants to leave this for now diff --git a/pkg/specgen/validate.go b/pkg/specgen/container_validate.go index 78e4d8ad5..b27659f5f 100644 --- a/pkg/specgen/validate.go +++ b/pkg/specgen/container_validate.go @@ -3,7 +3,7 @@ package specgen import ( "strings" - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" ) @@ -23,7 +23,7 @@ func exclusiveOptions(opt1, opt2 string) error { // Validate verifies that the given SpecGenerator is valid and satisfies required // input for creating a container. -func (s *SpecGenerator) validate(rt *libpod.Runtime) error { +func (s *SpecGenerator) validate() error { // // ContainerBasicConfig @@ -138,9 +138,6 @@ func (s *SpecGenerator) validate(rt *libpod.Runtime) error { if err := s.IpcNS.validate(); err != nil { return err } - if err := s.NetNS.validate(); err != nil { - return err - } if err := s.PidNS.validate(); err != nil { return err } @@ -155,5 +152,16 @@ func (s *SpecGenerator) validate(rt *libpod.Runtime) error { if len(s.WorkDir) < 1 { s.WorkDir = "/" } + + // Set defaults if network info is not provided + if s.NetNS.NSMode == "" { + s.NetNS.NSMode = Bridge + if rootless.IsRootless() { + s.NetNS.NSMode = Slirp + } + } + if err := validateNetNS(&s.NetNS); err != nil { + return err + } return nil } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 17b180cde..fa2dee77d 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -3,9 +3,9 @@ package specgen import ( "os" + "github.com/containers/common/pkg/capabilities" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" - "github.com/containers/libpod/pkg/capabilities" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" @@ -70,9 +70,7 @@ func (n *Namespace) IsPrivate() bool { return n.NSMode == Private } -// validate perform simple validation on the namespace to make sure it is not -// invalid from the get-go -func (n *Namespace) validate() error { +func validateNetNS(n *Namespace) error { if n == nil { return nil } @@ -82,6 +80,15 @@ func (n *Namespace) validate() error { default: return errors.Errorf("invalid network %q", n.NSMode) } + return nil +} + +// validate perform simple validation on the namespace to make sure it is not +// invalid from the get-go +func (n *Namespace) validate() error { + if n == nil { + return nil + } // Path and From Container MUST have a string value set if n.NSMode == Path || n.NSMode == FromContainer { if len(n.Value) < 1 { diff --git a/pkg/specgen/pod_create.go b/pkg/specgen/pod_create.go new file mode 100644 index 000000000..06aa24e22 --- /dev/null +++ b/pkg/specgen/pod_create.go @@ -0,0 +1,83 @@ +package specgen + +import ( + "context" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/sirupsen/logrus" +) + +func (p *PodSpecGenerator) MakePod(rt *libpod.Runtime) (*libpod.Pod, error) { + if err := p.validate(); err != nil { + return nil, err + } + options, err := p.createPodOptions() + if err != nil { + return nil, err + } + return rt.NewPod(context.Background(), options...) +} + +func (p *PodSpecGenerator) createPodOptions() ([]libpod.PodCreateOption, error) { + var ( + options []libpod.PodCreateOption + ) + if !p.NoInfra { + options = append(options, libpod.WithInfraContainer()) + nsOptions, err := shared.GetNamespaceOptions(p.SharedNamespaces) + if err != nil { + return nil, err + } + options = append(options, nsOptions...) + } + if len(p.CgroupParent) > 0 { + options = append(options, libpod.WithPodCgroupParent(p.CgroupParent)) + } + if len(p.Labels) > 0 { + options = append(options, libpod.WithPodLabels(p.Labels)) + } + if len(p.Name) > 0 { + options = append(options, libpod.WithPodName(p.Name)) + } + if len(p.Hostname) > 0 { + options = append(options, libpod.WithPodHostname(p.Hostname)) + } + if len(p.HostAdd) > 0 { + options = append(options, libpod.WithPodHosts(p.HostAdd)) + } + if len(p.DNSOption) > 0 { + options = append(options, libpod.WithPodDNSOption(p.DNSOption)) + } + if len(p.DNSSearch) > 0 { + options = append(options, libpod.WithPodDNSSearch(p.DNSSearch)) + } + if p.StaticIP != nil { + options = append(options, libpod.WithPodStaticIP(*p.StaticIP)) + } + if p.StaticMAC != nil { + options = append(options, libpod.WithPodStaticMAC(*p.StaticMAC)) + } + if p.NoManageResolvConf { + options = append(options, libpod.WithPodUseImageResolvConf()) + } + switch p.NetNS.NSMode { + case Bridge: + logrus.Debugf("Pod using default network mode") + case Host: + logrus.Debugf("Pod will use host networking") + options = append(options, libpod.WithPodHostNetwork()) + default: + logrus.Debugf("Pod joining CNI networks: %v", p.CNINetworks) + options = append(options, libpod.WithPodNetworks(p.CNINetworks)) + } + + if p.NoManageHosts { + options = append(options, libpod.WithPodUseImageHosts()) + } + if len(p.PortMappings) > 0 { + options = append(options, libpod.WithInfraContainerPorts(p.PortMappings)) + } + options = append(options, libpod.WithPodCgroups()) + return options, nil +} diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go new file mode 100644 index 000000000..50309f096 --- /dev/null +++ b/pkg/specgen/pod_validate.go @@ -0,0 +1,104 @@ +package specgen + +import ( + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" +) + +var ( + // ErrInvalidPodSpecConfig describes an error given when the podspecgenerator is invalid + ErrInvalidPodSpecConfig error = errors.New("invalid pod spec") +) + +func exclusivePodOptions(opt1, opt2 string) error { + return errors.Wrapf(ErrInvalidPodSpecConfig, "%s and %s are mutually exclusive pod options", opt1, opt2) +} + +func (p *PodSpecGenerator) validate() error { + // PodBasicConfig + if p.NoInfra { + if len(p.InfraCommand) > 0 { + return exclusivePodOptions("NoInfra", "InfraCommand") + } + if len(p.InfraImage) > 0 { + return exclusivePodOptions("NoInfra", "InfraImage") + } + if len(p.SharedNamespaces) > 0 { + return exclusivePodOptions("NoInfo", "SharedNamespaces") + } + } + + // PodNetworkConfig + if err := p.NetNS.validate(); err != nil { + return err + } + if p.NoInfra { + if p.NetNS.NSMode == NoNetwork { + return errors.New("NoInfra and a none network cannot be used toegther") + } + if p.StaticIP != nil { + return exclusivePodOptions("NoInfra", "StaticIP") + } + if p.StaticMAC != nil { + return exclusivePodOptions("NoInfra", "StaticMAC") + } + if len(p.DNSOption) > 0 { + return exclusivePodOptions("NoInfra", "DNSOption") + } + if len(p.DNSSearch) > 0 { + return exclusivePodOptions("NoInfo", "DNSSearch") + } + if len(p.DNSServer) > 0 { + return exclusivePodOptions("NoInfra", "DNSServer") + } + if len(p.HostAdd) > 0 { + return exclusivePodOptions("NoInfra", "HostAdd") + } + if p.NoManageResolvConf { + return exclusivePodOptions("NoInfra", "NoManageResolvConf") + } + } + if p.NetNS.NSMode != Bridge { + if len(p.PortMappings) > 0 { + return errors.New("PortMappings can only be used with Bridge mode networking") + } + if len(p.CNINetworks) > 0 { + return errors.New("CNINetworks can only be used with Bridge mode networking") + } + } + if p.NoManageResolvConf { + if len(p.DNSServer) > 0 { + return exclusivePodOptions("NoManageResolvConf", "DNSServer") + } + if len(p.DNSSearch) > 0 { + return exclusivePodOptions("NoManageResolvConf", "DNSSearch") + } + if len(p.DNSOption) > 0 { + return exclusivePodOptions("NoManageResolvConf", "DNSOption") + } + } + if p.NoManageHosts && len(p.HostAdd) > 0 { + return exclusivePodOptions("NoManageHosts", "HostAdd") + } + + if err := p.NetNS.validate(); err != nil { + return err + } + + // Set Defaults + if p.NetNS.Value == "" { + if rootless.IsRootless() { + p.NetNS.NSMode = Slirp + } else { + p.NetNS.NSMode = Bridge + } + } + if len(p.InfraImage) < 1 { + p.InfraImage = define.DefaultInfraImage + } + if len(p.InfraCommand) < 1 { + p.InfraCommand = []string{define.DefaultInfraCommand} + } + return nil +} diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go new file mode 100644 index 000000000..3f830014d --- /dev/null +++ b/pkg/specgen/podspecgen.go @@ -0,0 +1,153 @@ +package specgen + +import ( + "net" + + "github.com/cri-o/ocicni/pkg/ocicni" +) + +// PodBasicConfig contains basic configuration options for pods. +type PodBasicConfig struct { + // Name is the name of the pod. + // If not provided, a name will be generated when the pod is created. + // Optional. + Name string `json:"name,omitempty"` + // Hostname is the pod's hostname. If not set, the name of the pod will + // be used (if a name was not provided here, the name auto-generated for + // the pod will be used). This will be used by the infra container and + // all containers in the pod as long as the UTS namespace is shared. + // Optional. + Hostname string `json:"hostname,omitempty"` + // Labels are key-value pairs that are used to add metadata to pods. + // Optional. + Labels map[string]string `json:"labels,omitempty"` + // NoInfra tells the pod not to create an infra container. If this is + // done, many networking-related options will become unavailable. + // Conflicts with setting any options in PodNetworkConfig, and the + // InfraCommand and InfraImages in this struct. + // Optional. + NoInfra bool `json:"no_infra,omitempty"` + // InfraCommand sets the command that will be used to start the infra + // container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraCommand []string `json:"infra_command,omitempty"` + // InfraImage is the image that will be used for the infra container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraImage string `json:"infra_image,omitempty"` + // SharedNamespaces instructs the pod to share a set of namespaces. + // Shared namespaces will be joined (by default) by every container + // which joins the pod. + // If not set and NoInfra is false, the pod will set a default set of + // namespaces to share. + // Conflicts with NoInfra=true. + // Optional. + SharedNamespaces []string `json:"shared_namespaces,omitempty"` +} + +// PodNetworkConfig contains networking configuration for a pod. +type PodNetworkConfig struct { + // NetNS is the configuration to use for the infra container's network + // namespace. This network will, by default, be shared with all + // containers in the pod. + // Cannot be set to FromContainer and FromPod. + // Setting this to anything except "" conflicts with NoInfra=true. + // Defaults to Bridge as root and Slirp as rootless. + // Mandatory. + NetNS Namespace `json:"netns,omitempty"` + // StaticIP sets a static IP for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static IP for the whole pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticIP *net.IP `json:"static_ip,omitempty"` + // StaticMAC sets a static MAC for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static MAC for the entire pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + // PortMappings is a set of ports to map into the infra container. + // As, by default, containers share their network with the infra + // container, this will forward the ports to the entire pod. + // Only available if NetNS is set to Bridge or Slirp. + // Optional. + PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"` + // CNINetworks is a list of CNI networks that the infra container will + // join. As, by default, containers share their network with the infra + // container, these networks will effectively be joined by the + // entire pod. + // Only available when NetNS is set to Bridge, the default for root. + // Optional. + CNINetworks []string `json:"cni_networks,omitempty"` + // NoManageResolvConf indicates that /etc/resolv.conf should not be + // managed by the pod. Instead, each container will create and manage a + // separate resolv.conf as if they had not joined a pod. + // Conflicts with NoInfra=true and DNSServer, DNSSearch, DNSOption. + // Optional. + NoManageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` + // DNSServer is a set of DNS servers that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // If not provided, the host's DNS servers will be used, unless the only + // server set is a localhost address. As the container cannot connect to + // the host's localhost, a default server will instead be set. + // Conflicts with NoInfra=true. + // Optional. + DNSServer []net.IP `json:"dns_server,omitempty"` + // DNSSearch is a set of DNS search domains that will be used in the + // infra container's resolv.conf, which will, by default, be shared with + // all containers in the pod. + // If not provided, DNS search domains from the host's resolv.conf will + // be used. + // Conflicts with NoInfra=true. + // Optional. + DNSSearch []string `json:"dns_search,omitempty"` + // DNSOption is a set of DNS options that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // Conflicts with NoInfra=true. + // Optional. + DNSOption []string `json:"dns_option,omitempty"` + // NoManageHosts indicates that /etc/hosts should not be managed by the + // pod. Instead, each container will create a separate /etc/hosts as + // they would if not in a pod. + // Conflicts with HostAdd. + NoManageHosts bool `json:"no_manage_hosts,omitempty"` + // HostAdd is a set of hosts that will be added to the infra container's + // /etc/hosts that will, by default, be shared with all containers in + // the pod. + // Conflicts with NoInfra=true and NoManageHosts. + // Optional. + HostAdd []string `json:"hostadd,omitempty"` +} + +// PodCgroupConfig contains configuration options about a pod's cgroups. +// This will be expanded in future updates to pods. +type PodCgroupConfig struct { + // CgroupParent is the parent for the CGroup that the pod will create. + // This pod cgroup will, in turn, be the default cgroup parent for all + // containers in the pod. + // Optional. + CgroupParent string `json:"cgroup_parent,omitempty"` +} + +// PodSpecGenerator describes options to create a pod +// swagger:model PodSpecGenerator +type PodSpecGenerator struct { + PodBasicConfig + PodNetworkConfig + PodCgroupConfig +} + +// NewPodSpecGenerator creates a new pod spec +func NewPodSpecGenerator() *PodSpecGenerator { + return &PodSpecGenerator{} +} diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index e1dfe4dc5..89c76c273 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -53,7 +53,7 @@ type ContainerBasicConfig struct { Terminal bool `json:"terminal,omitempty"` // Stdin is whether the container will keep its STDIN open. Stdin bool `json:"stdin,omitempty"` - // Labels are key-valid labels that are used to add metadata to + // Labels are key-value pairs that are used to add metadata to // containers. // Optional. Labels map[string]string `json:"labels,omitempty"` @@ -143,6 +143,10 @@ type ContainerStorageConfig struct { // Conflicts with Rootfs. // At least one of Image or Rootfs must be specified. Image string `json:"image"` + // RawImageName is the unprocessed and not-normalized user-specified image + // name. One use case for having this data at hand are auto-updates where + // the _exact_ user input is needed in order to look-up the correct image. + RawImageName string `json:"raw_image_name,omitempty"` // Rootfs is the path to a directory that will be used as the // container's root filesystem. No modification will be made to the // directory, it will be directly mounted into the container as root. @@ -390,18 +394,18 @@ type SpecGenerator struct { // NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs func NewSpecGenerator(image string) *SpecGenerator { - net := ContainerNetworkConfig{ + networkConfig := ContainerNetworkConfig{ NetNS: Namespace{ NSMode: Bridge, }, } csc := ContainerStorageConfig{Image: image} if rootless.IsRootless() { - net.NetNS.NSMode = Slirp + networkConfig.NetNS.NSMode = Slirp } return &SpecGenerator{ ContainerStorageConfig: csc, - ContainerNetworkConfig: net, + ContainerNetworkConfig: networkConfig, } } diff --git a/pkg/systemd/dbus.go b/pkg/systemd/dbus.go new file mode 100644 index 000000000..df24667a1 --- /dev/null +++ b/pkg/systemd/dbus.go @@ -0,0 +1,47 @@ +package systemd + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/containers/libpod/pkg/rootless" + "github.com/coreos/go-systemd/v22/dbus" + godbus "github.com/godbus/dbus/v5" +) + +func dbusAuthRootlessConnection(createBus func(opts ...godbus.ConnOption) (*godbus.Conn, error)) (*godbus.Conn, error) { + conn, err := createBus() + if err != nil { + return nil, err + } + + methods := []godbus.Auth{godbus.AuthExternal(strconv.Itoa(rootless.GetRootlessUID()))} + + err = conn.Auth(methods) + if err != nil { + conn.Close() + return nil, err + } + + return conn, nil +} + +func newRootlessConnection() (*dbus.Conn, error) { + return dbus.NewConnection(func() (*godbus.Conn, error) { + return dbusAuthRootlessConnection(func(opts ...godbus.ConnOption) (*godbus.Conn, error) { + path := filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "systemd/private") + return godbus.Dial(fmt.Sprintf("unix:path=%s", path)) + }) + }) +} + +// ConnectToDBUS returns a DBUS connection. It works both as root and non-root +// users. +func ConnectToDBUS() (*dbus.Conn, error) { + if rootless.IsRootless() { + return newRootlessConnection() + } + return dbus.NewSystemdConnection() +} diff --git a/pkg/systemd/generate/systemdgen.go b/pkg/systemd/generate/systemdgen.go index 404347828..eb15d4927 100644 --- a/pkg/systemd/generate/systemdgen.go +++ b/pkg/systemd/generate/systemdgen.go @@ -16,6 +16,10 @@ import ( "github.com/sirupsen/logrus" ) +// EnvVariable "PODMAN_SYSTEMD_UNIT" is set in all generated systemd units and +// is set to the unit's (unique) name. +const EnvVariable = "PODMAN_SYSTEMD_UNIT" + // ContainerInfo contains data required for generating a container's systemd // unit file. type ContainerInfo struct { @@ -57,6 +61,8 @@ type ContainerInfo struct { // RunCommand is a post-processed variant of CreateCommand and used for // the ExecStart field in generic unit files. RunCommand string + // EnvVariable is generate.EnvVariable and must not be set. + EnvVariable string } var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} @@ -80,6 +86,8 @@ const containerTemplate = `# {{.ServiceName}}.service [Unit] Description=Podman {{.ServiceName}}.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target {{- if .BoundToServices}} RefuseManualStart=yes RefuseManualStop=yes @@ -92,13 +100,14 @@ Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ {{- end}} [Service] +Environment={{.EnvVariable}}=%n Restart={{.RestartPolicy}} {{- if .New}} -ExecStartPre=/usr/bin/rm -f /%t/%n-pid /%t/%n-cid +ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid ExecStart={{.RunCommand}} -ExecStop={{.Executable}} stop --ignore --cidfile /%t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} -ExecStopPost={{.Executable}} rm --ignore -f --cidfile /%t/%n-cid -PIDFile=/%t/%n-pid +ExecStop={{.Executable}} stop --ignore --cidfile %t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} +ExecStopPost={{.Executable}} rm --ignore -f --cidfile %t/%n-cid +PIDFile=%t/%n-pid {{- else}} ExecStart={{.Executable}} start {{.ContainerName}} ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}} @@ -108,7 +117,7 @@ KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` // Options include different options to control the unit file generation. type Options struct { @@ -136,6 +145,8 @@ func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, erro info.Executable = executable } + info.EnvVariable = EnvVariable + // Assemble the ExecStart command when creating a new container. // // Note that we cannot catch all corner cases here such that users @@ -158,10 +169,30 @@ func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, erro command := []string{ info.Executable, "run", - "--conmon-pidfile", "/%t/%n-pid", - "--cidfile", "/%t/%n-cid", + "--conmon-pidfile", "%t/%n-pid", + "--cidfile", "%t/%n-cid", "--cgroups=no-conmon", } + + // Enforce detaching + // + // since we use systemd `Type=forking` service + // @see https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= + // when we generated systemd service file with the --new param, + // `ExecStart` will have `/usr/bin/podman run ...` + // if `info.CreateCommand` has no `-d` or `--detach` param, + // podman will run the container in default attached mode, + // as a result, `systemd start` will wait the `podman run` command exit until failed with timeout error. + hasDetachParam := false + for _, p := range info.CreateCommand[index:] { + if p == "--detach" || p == "-d" { + hasDetachParam = true + } + } + if !hasDetachParam { + command = append(command, "-d") + } + command = append(command, info.CreateCommand[index:]...) info.RunCommand = strings.Join(command, " ") info.New = true diff --git a/pkg/systemd/generate/systemdgen_test.go b/pkg/systemd/generate/systemdgen_test.go index b74b75258..3269405a6 100644 --- a/pkg/systemd/generate/systemdgen_test.go +++ b/pkg/systemd/generate/systemdgen_test.go @@ -40,8 +40,11 @@ func TestCreateContainerSystemdUnit(t *testing.T) { [Unit] Description=Podman container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 ExecStop=/usr/bin/podman stop -t 10 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 @@ -50,7 +53,7 @@ KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` goodName := `# container-foobar.service # autogenerated by Podman CI @@ -58,8 +61,11 @@ WantedBy=multi-user.target` [Unit] Description=Podman container-foobar.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar @@ -68,7 +74,7 @@ KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` goodNameBoundTo := `# container-foobar.service # autogenerated by Podman CI @@ -76,12 +82,15 @@ WantedBy=multi-user.target` [Unit] Description=Podman container-foobar.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target RefuseManualStart=yes RefuseManualStop=yes BindsTo=a.service b.service c.service pod.service After=a.service b.service c.service pod.service [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar @@ -90,7 +99,7 @@ KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` podGoodName := `# pod-123abc.service # autogenerated by Podman CI @@ -98,10 +107,13 @@ WantedBy=multi-user.target` [Unit] Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target Requires=container-1.service container-2.service Before=container-1.service container-2.service [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start jadda-jadda-infra ExecStop=/usr/bin/podman stop -t 10 jadda-jadda-infra @@ -110,7 +122,7 @@ KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` goodNameNew := `# jadda-jadda.service # autogenerated by Podman CI @@ -118,19 +130,68 @@ WantedBy=multi-user.target` [Unit] Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always -ExecStartPre=/usr/bin/rm -f /%t/%n-pid /%t/%n-cid -ExecStart=/usr/bin/podman run --conmon-pidfile /%t/%n-pid --cidfile /%t/%n-cid --cgroups=no-conmon --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN -ExecStop=/usr/bin/podman stop --ignore --cidfile /%t/%n-cid -t 42 -ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile /%t/%n-cid -PIDFile=/%t/%n-pid +ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid +ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-cid --cgroups=no-conmon -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-cid -t 42 +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-cid +PIDFile=%t/%n-pid KillMode=none Type=forking [Install] -WantedBy=multi-user.target` +WantedBy=multi-user.target default.target` + + goodNameNewDetach := `# jadda-jadda.service +# autogenerated by Podman CI + +[Unit] +Description=Podman jadda-jadda.service +Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=always +ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid +ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-cid --cgroups=no-conmon --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-cid -t 42 +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-cid +PIDFile=%t/%n-pid +KillMode=none +Type=forking + +[Install] +WantedBy=multi-user.target default.target` + + goodIdNew := `# container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service +Documentation=man:podman-generate-systemd(1) +Wants=network.target +After=network-online.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=always +ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid +ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-cid --cgroups=no-conmon -d awesome-image:latest +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-cid -t 10 +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-cid +PIDFile=%t/%n-pid +KillMode=none +Type=forking + +[Install] +WantedBy=multi-user.target default.target` tests := []struct { name string @@ -220,6 +281,51 @@ WantedBy=multi-user.target` goodNameNew, false, }, + {"good with explicit short detach param", + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "jadda-jadda", + ContainerName: "jadda-jadda", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + New: true, + CreateCommand: []string{"I'll get stripped", "container", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, + }, + goodNameNew, + false, + }, + {"good with explicit full detach param", + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "jadda-jadda", + ContainerName: "jadda-jadda", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + New: true, + CreateCommand: []string{"I'll get stripped", "container", "run", "--detach", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, + }, + goodNameNewDetach, + false, + }, + {"good with id and no param", + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", + ContainerName: "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + New: true, + CreateCommand: []string{"I'll get stripped", "container", "run", "awesome-image:latest"}, + }, + goodIdNew, + false, + }, } for _, tt := range tests { test := tt diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 4a52ea68d..0c055745d 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -309,15 +309,15 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { // Strip off leading dash, to allow -1 or -HUP basename := strings.TrimPrefix(rawSignal, "-") - signal, err := signal.ParseSignal(basename) + sig, err := signal.ParseSignal(basename) if err != nil { return -1, err } // 64 is SIGRTMAX; wish we could get this from a standard Go library - if signal < 1 || signal > 64 { + if sig < 1 || sig > 64 { return -1, errors.Errorf("valid signals are 1 through 64") } - return signal, nil + return sig, nil } // ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping @@ -600,3 +600,55 @@ func HomeDir() (string, error) { } return home, nil } + +func Tmpdir() string { + tmpdir := os.Getenv("TMPDIR") + if tmpdir == "" { + tmpdir = "/var/tmp" + } + + return tmpdir +} + +// ValidateSysctls validates a list of sysctl and returns it. +func ValidateSysctls(strSlice []string) (map[string]string, error) { + sysctl := make(map[string]string) + validSysctlMap := map[string]bool{ + "kernel.msgmax": true, + "kernel.msgmnb": true, + "kernel.msgmni": true, + "kernel.sem": true, + "kernel.shmall": true, + "kernel.shmmax": true, + "kernel.shmmni": true, + "kernel.shm_rmid_forced": true, + } + validSysctlPrefixes := []string{ + "net.", + "fs.mqueue.", + } + + for _, val := range strSlice { + foundMatch := false + arr := strings.Split(val, "=") + if len(arr) < 2 { + return nil, errors.Errorf("%s is invalid, sysctl values must be in the form of KEY=VALUE", val) + } + if validSysctlMap[arr[0]] { + sysctl[arr[0]] = arr[1] + continue + } + + for _, prefix := range validSysctlPrefixes { + if strings.HasPrefix(arr[0], prefix) { + sysctl[arr[0]] = arr[1] + foundMatch = true + break + } + } + if !foundMatch { + return nil, errors.Errorf("sysctl '%s' is not whitelisted", arr[0]) + } + } + return sysctl, nil +} diff --git a/pkg/util/utils_linux_test.go b/pkg/util/utils_linux_test.go new file mode 100644 index 000000000..38e6dbef9 --- /dev/null +++ b/pkg/util/utils_linux_test.go @@ -0,0 +1,29 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetImageConfigStopSignal(t *testing.T) { + // Linux-only beause parsing signal names is not supported on non-Linux systems by + // pkg/signal. + stopSignalValidInt, err := GetImageConfig([]string{"STOPSIGNAL 9"}) + require.Nil(t, err) + assert.Equal(t, stopSignalValidInt.StopSignal, "9") + + stopSignalValidString, err := GetImageConfig([]string{"STOPSIGNAL SIGKILL"}) + require.Nil(t, err) + assert.Equal(t, stopSignalValidString.StopSignal, "9") + + _, err = GetImageConfig([]string{"STOPSIGNAL 0"}) + assert.NotNil(t, err) + + _, err = GetImageConfig([]string{"STOPSIGNAL garbage"}) + assert.NotNil(t, err) + + _, err = GetImageConfig([]string{"STOPSIGNAL "}) + assert.NotNil(t, err) +} diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go index f4b03599d..a9b37844e 100644 --- a/pkg/util/utils_test.go +++ b/pkg/util/utils_test.go @@ -219,25 +219,6 @@ func TestGetImageConfigLabel(t *testing.T) { assert.NotNil(t, err) } -func TestGetImageConfigStopSignal(t *testing.T) { - stopSignalValidInt, err := GetImageConfig([]string{"STOPSIGNAL 9"}) - require.Nil(t, err) - assert.Equal(t, stopSignalValidInt.StopSignal, "9") - - stopSignalValidString, err := GetImageConfig([]string{"STOPSIGNAL SIGKILL"}) - require.Nil(t, err) - assert.Equal(t, stopSignalValidString.StopSignal, "9") - - _, err = GetImageConfig([]string{"STOPSIGNAL 0"}) - assert.NotNil(t, err) - - _, err = GetImageConfig([]string{"STOPSIGNAL garbage"}) - assert.NotNil(t, err) - - _, err = GetImageConfig([]string{"STOPSIGNAL "}) - assert.NotNil(t, err) -} - func TestGetImageConfigOnBuild(t *testing.T) { onBuildOne, err := GetImageConfig([]string{"ONBUILD ADD /testdir1"}) require.Nil(t, err) @@ -264,3 +245,15 @@ func TestGetImageConfigMisc(t *testing.T) { _, err = GetImageConfig([]string{"BADINST testvalue"}) assert.NotNil(t, err) } + +func TestValidateSysctls(t *testing.T) { + strSlice := []string{"net.core.test1=4", "kernel.msgmax=2"} + result, _ := ValidateSysctls(strSlice) + assert.Equal(t, result["net.core.test1"], "4") +} + +func TestValidateSysctlBadSysctl(t *testing.T) { + strSlice := []string{"BLAU=BLUE", "GELB^YELLOW"} + _, err := ValidateSysctls(strSlice) + assert.Error(t, err) +} diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 94726bbbd..55427771c 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -846,11 +846,6 @@ func (i *LibpodAPI) ExecContainer(call iopodman.VarlinkCall, opts iopodman.ExecO workDir = *opts.Workdir } - var detachKeys string - if opts.DetachKeys != nil { - detachKeys = *opts.DetachKeys - } - resizeChan := make(chan remotecommand.TerminalSize) reader, writer, _, pipeWriter, streams := setupStreams(call) @@ -870,8 +865,17 @@ func (i *LibpodAPI) ExecContainer(call iopodman.VarlinkCall, opts iopodman.ExecO } }() + execConfig := new(libpod.ExecConfig) + execConfig.Command = opts.Cmd + execConfig.Terminal = opts.Tty + execConfig.Privileged = opts.Privileged + execConfig.Environment = envs + execConfig.User = user + execConfig.WorkDir = workDir + execConfig.DetachKeys = opts.DetachKeys + go func() { - ec, err := ctr.Exec(opts.Tty, opts.Privileged, envs, opts.Cmd, user, workDir, streams, 0, resizeChan, detachKeys) + ec, err := ctr.Exec(execConfig, streams, resizeChan) if err != nil { logrus.Errorf(err.Error()) } diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index c4809f16b..82587f5c4 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -30,7 +30,6 @@ import ( "github.com/containers/libpod/utils" "github.com/containers/storage/pkg/archive" v1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -147,7 +146,6 @@ func (i *LibpodAPI) BuildImage(call iopodman.VarlinkCall, config iopodman.BuildI err error ) - systemContext := types.SystemContext{} contextDir := config.ContextDir newContextDir, err := ioutil.TempDir("", "buildTarball") @@ -175,6 +173,8 @@ func (i *LibpodAPI) BuildImage(call iopodman.VarlinkCall, config iopodman.BuildI logrus.Errorf("unable to delete directory '%s': %q", newContextDir, err) } }() + + systemContext := types.SystemContext{} // All output (stdout, stderr) is captured in output as well var output bytes.Buffer @@ -192,40 +192,40 @@ func (i *LibpodAPI) BuildImage(call iopodman.VarlinkCall, config iopodman.BuildI Volumes: config.BuildOptions.Volume, } - hostNetwork := buildah.NamespaceOption{ - Name: string(specs.NetworkNamespace), - Host: true, - } - - namespace = append(namespace, hostNetwork) - options := imagebuildah.BuildOptions{ - CommonBuildOpts: commonOpts, + AddCapabilities: config.AddCapabilities, AdditionalTags: config.AdditionalTags, Annotations: config.Annotations, + Architecture: config.Architecture, Args: config.BuildArgs, CNIConfigDir: config.CniConfigDir, CNIPluginPath: config.CniPluginDir, + CommonBuildOpts: commonOpts, Compression: stringCompressionToArchiveType(config.Compression), ContextDirectory: newContextDir, DefaultMountsFilePath: config.DefaultsMountFilePath, + Devices: config.Devices, Err: &output, ForceRmIntermediateCtrs: config.ForceRmIntermediateCtrs, IIDFile: config.Iidfile, Labels: config.Label, Layers: config.Layers, + NamespaceOptions: namespace, NoCache: config.Nocache, + OS: config.Os, Out: &output, Output: config.Output, - NamespaceOptions: namespace, OutputFormat: config.OutputFormat, PullPolicy: stringPullPolicyToType(config.PullPolicy), Quiet: config.Quiet, RemoveIntermediateCtrs: config.RemoteIntermediateCtrs, ReportWriter: &output, RuntimeArgs: config.RuntimeArgs, + SignBy: config.SignBy, Squash: config.Squash, SystemContext: &systemContext, + Target: config.Target, + TransientMounts: config.TransientMounts, } if call.WantsMore() { @@ -587,7 +587,7 @@ func (i *LibpodAPI) Commit(call iopodman.VarlinkCall, name, imageName string, ch if err != nil { return call.ReplyErrorOccurred(err.Error()) } - sc := image.GetSystemContext(rtc.SignaturePolicyPath, "", false) + sc := image.GetSystemContext(rtc.Engine.SignaturePolicyPath, "", false) switch manifestType { case "oci", "": // nolint mimeType = buildah.OCIv1ImageManifest @@ -597,7 +597,7 @@ func (i *LibpodAPI) Commit(call iopodman.VarlinkCall, name, imageName string, ch return call.ReplyErrorOccurred(fmt.Sprintf("unrecognized image format %q", manifestType)) } coptions := buildah.CommitOptions{ - SignaturePolicyPath: rtc.SignaturePolicyPath, + SignaturePolicyPath: rtc.Engine.SignaturePolicyPath, ReportWriter: output, SystemContext: sc, PreferredManifestType: mimeType, @@ -688,12 +688,18 @@ func (i *LibpodAPI) ExportImage(call iopodman.VarlinkCall, name, destination str } // PullImage pulls an image from a registry to the image store. -func (i *LibpodAPI) PullImage(call iopodman.VarlinkCall, name string) error { +func (i *LibpodAPI) PullImage(call iopodman.VarlinkCall, name string, creds iopodman.AuthConfig) error { var ( imageID string err error ) - dockerRegistryOptions := image.DockerRegistryOptions{} + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerRegistryCreds: &types.DockerAuthConfig{ + Username: creds.Username, + Password: creds.Password, + }, + } + so := image.SigningOptions{} if call.WantsMore() { diff --git a/pkg/varlinkapi/system.go b/pkg/varlinkapi/system.go index 50aaaaa44..e88d010c5 100644 --- a/pkg/varlinkapi/system.go +++ b/pkg/varlinkapi/system.go @@ -10,7 +10,7 @@ import ( "time" "github.com/containers/image/v5/pkg/sysregistriesv2" - "github.com/containers/libpod/cmd/podman/varlink" + iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod/define" "github.com/sirupsen/logrus" ) diff --git a/pkg/varlinkapi/volumes.go b/pkg/varlinkapi/volumes.go index 2dddd3008..e497cb537 100644 --- a/pkg/varlinkapi/volumes.go +++ b/pkg/varlinkapi/volumes.go @@ -6,7 +6,7 @@ import ( "encoding/json" "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/cmd/podman/varlink" + iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" ) @@ -105,16 +105,20 @@ func (i *LibpodAPI) InspectVolume(call iopodman.VarlinkCall, name string) error // VolumesPrune removes unused images via a varlink call func (i *LibpodAPI) VolumesPrune(call iopodman.VarlinkCall) error { - var errs []string - prunedNames, prunedErrors := i.Runtime.PruneVolumes(getContext()) - if len(prunedErrors) == 0 { - return call.ReplyVolumesPrune(prunedNames, []string{}) + var ( + prunedErrors []string + prunedNames []string + ) + responses, err := i.Runtime.PruneVolumes(getContext()) + if err != nil { + return call.ReplyVolumesPrune([]string{}, []string{err.Error()}) } - - // We need to take the errors and capture their strings to go back over - // varlink - for _, e := range prunedErrors { - errs = append(errs, e.Error()) + for k, v := range responses { + if v == nil { + prunedNames = append(prunedNames, k) + } else { + prunedErrors = append(prunedErrors, v.Error()) + } } - return call.ReplyVolumesPrune(prunedNames, errs) + return call.ReplyVolumesPrune(prunedNames, prunedErrors) } |