diff options
121 files changed, 9585 insertions, 89 deletions
@@ -153,7 +153,7 @@ lint: .gopathok varlink_generate ## Execute the source code linter @./.tool/lint golangci-lint: .gopathok varlink_generate .install.golangci-lint - $(GOBIN)/golangci-lint run --tests=false + $(GOBIN)/golangci-lint run --tests=false --skip-files swagger.go gofmt: ## Verify the source code gofmt find . -name '*.go' ! -path './vendor/*' -exec gofmt -s -w {} \+ @@ -194,6 +194,19 @@ bin/podman.cross.%: .gopathok GOARCH="$${TARGET##*.}" \ $(GO_BUILD) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags '$(BUILDTAGS_CROSS)' -o "$@" $(PROJECT)/cmd/podman +.PHONY: service +service: .gopathok + $(GO_BUILD) $(BUILDFLAGS) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS)" -o bin/$@ $(PROJECT)/cmd/service + +.PHONY: +run-service: + systemd-socket-activate -l 8080 ./bin/service + +.PHONY: run-docker-py-tests +run-docker-py-tests: + $(eval testLogs=$(shell mktemp)) + ./bin/podman run --rm --security-opt label=disable --privileged -v $(testLogs):/testLogs --net=host -e DOCKER_HOST=tcp://localhost:8080 $(DOCKERPY_IMAGE) sh -c "pytest $(DOCKERPY_TEST) " + clean: ## Clean artifacts rm -rf \ .gopathok \ @@ -452,7 +465,6 @@ install.systemd: install ${SELINUXOPT} -m 644 contrib/varlink/podman.conf ${DESTDIR}${TMPFILESDIR}/podman.conf uninstall: - # Remove manpages for i in $(filter %.1,$(MANPAGES_DEST)); do \ rm -f $(DESTDIR)$(MANDIR)/man1/$$(basename $${i}); \ done; \ @@ -520,7 +532,7 @@ install.libseccomp.sudo: cd ../../seccomp/libseccomp && git checkout --detach $(LIBSECCOMP_COMMIT) && ./autogen.sh && ./configure --prefix=/usr && make all && make install -cmd/podman/varlink/iopodman.go: cmd/podman/varlink/io.podman.varlink +cmd/podman/varlink/iopodman.go: .gopathok cmd/podman/varlink/io.podman.varlink GO111MODULE=off $(GO) generate ./cmd/podman/varlink/... API.md: cmd/podman/varlink/io.podman.varlink diff --git a/cmd/podman/build.go b/cmd/podman/build.go index fbf85fc97..08d3edaa3 100644 --- a/cmd/podman/build.go +++ b/cmd/podman/build.go @@ -375,7 +375,8 @@ func buildCmd(c *cliconfig.BuildValues) error { }, Target: c.Target, } - return runtime.Build(getContext(), c, options, containerfiles) + _, _, err = runtime.Build(getContext(), c, options, containerfiles) + return err } // useLayers returns false if BUILDAH_LAYERS is set to "0" or "false" diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index 0e4315411..b261599e6 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -163,6 +163,7 @@ type GenerateKubeValues struct { type GenerateSystemdValues struct { PodmanCommand Name bool + New bool Files bool RestartPolicy string StopTimeout int diff --git a/cmd/podman/generate_systemd.go b/cmd/podman/generate_systemd.go index aa202a68d..a9775f9cb 100644 --- a/cmd/podman/generate_systemd.go +++ b/cmd/podman/generate_systemd.go @@ -45,6 +45,7 @@ func init() { } flags.IntVarP(&containerSystemdCommand.StopTimeout, "timeout", "t", -1, "stop timeout override") flags.StringVar(&containerSystemdCommand.RestartPolicy, "restart-policy", "on-failure", "applicable systemd restart-policy") + flags.BoolVarP(&containerSystemdCommand.New, "new", "", false, "create a new container instead of starting an existing one") } func generateSystemdCmd(c *cliconfig.GenerateSystemdValues) error { diff --git a/cmd/podman/tree.go b/cmd/podman/tree.go index cb1b3fc9c..566f96995 100644 --- a/cmd/podman/tree.go +++ b/cmd/podman/tree.go @@ -56,7 +56,7 @@ func treeCmd(c *cliconfig.TreeValues) error { return errors.Wrapf(err, "error creating libpod runtime") } defer runtime.DeferredShutdown(false) - imageInfo, layerInfoMap, img, err := runtime.Tree(c) + imageInfo, layerInfoMap, img, err := runtime.Tree(c.InputArgs[0]) if err != nil { return err } diff --git a/cmd/service/main.go b/cmd/service/main.go new file mode 100644 index 000000000..0290de892 --- /dev/null +++ b/cmd/service/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/cmd/podman/libpodruntime" + api "github.com/containers/libpod/pkg/api/server" + "github.com/containers/storage/pkg/reexec" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func initConfig() { + // we can do more stuff in here. +} + +func main() { + if reexec.Init() { + // We were invoked with a different argv[0] indicating that we + // had a specific job to do as a subprocess, and it's done. + return + } + + cobra.OnInitialize(initConfig) + log.SetLevel(log.DebugLevel) + + config := cliconfig.PodmanCommand{ + Command: &cobra.Command{}, + InputArgs: []string{}, + GlobalFlags: cliconfig.MainFlags{}, + Remote: false, + } + // Create a single runtime for http + runtime, err := libpodruntime.GetRuntimeDisableFDs(context.Background(), &config) + if err != nil { + fmt.Printf("error creating libpod runtime: %s", err.Error()) + os.Exit(1) + } + defer runtime.DeferredShutdown(false) + + server, err := api.NewServer(runtime) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + err = server.Serve() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 85aa81553..fdc2edf39 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -419,6 +419,17 @@ Logging driver specific options. Used to set the path to the container log file `--log-opt path=/var/log/container/mycontainer.json` +**--log-opt**=*tag* + +Set custom logging configuration. Presently supports the `tag` option +which specified a custom log tag for the container. For example: + +`--log-opt tag="{{.ImageName}}"` + +It supports the same keys as `podman inspect --format`. + +It is currently supported only by the journald log driver. + **--mac-address**=*address* Container MAC address (e.g. 92:d0:c6:0a:29:33) @@ -575,7 +586,7 @@ To make a pod with more granular options, use the `podman pod create` command be Give extended privileges to this container. The default is *false*. By default, Podman containers are -“unprivileged” (=false) and cannot, for example, modify parts of the kernel. +“unprivileged” (=false) and cannot, for example, modify parts of the operating system. This is because by default a container is not allowed to access any devices. A “privileged” container is given access to all devices. @@ -584,6 +595,8 @@ to all devices on the host, turns off graphdriver mount options, as well as turning off most of the security measures protecting the host from the container. +Rootless containers cannot have more privileges than the account that launched them. + **--publish**, **-p**=*port* Publish a container's port, or range of ports, to the host diff --git a/docs/source/markdown/podman-exec.1.md b/docs/source/markdown/podman-exec.1.md index d46427c91..fc67211d1 100644 --- a/docs/source/markdown/podman-exec.1.md +++ b/docs/source/markdown/podman-exec.1.md @@ -43,7 +43,19 @@ Pass down to the process N additional file descriptors (in addition to 0, 1, 2). **--privileged** -Give the process extended Linux capabilities when running the command in container. +Give extended privileges to this container. The default is *false*. + +By default, Podman containers are +"unprivileged" and cannot, for example, modify parts of the operating system. +This is because by default a container is only allowed limited access to devices. +A "privileged" container is given the same access to devices as the user launching the container. + +A privileged container turns off the security features that isolate the +container from the host. Dropped Capabilities, limited devices, read/only mount +points, Apparmor/SELinux separation, and Seccomp filters are all disabled. + +Rootless containers cannot have more privileges than the account that launched them. + **--tty**, **-t** diff --git a/docs/source/markdown/podman-generate-systemd.1.md b/docs/source/markdown/podman-generate-systemd.1.md index b81e68a46..4d3f9ba48 100644 --- a/docs/source/markdown/podman-generate-systemd.1.md +++ b/docs/source/markdown/podman-generate-systemd.1.md @@ -22,11 +22,16 @@ Generate files instead of printing to stdout. The generated files are named {co Use the name of the container for the start, stop, and description in the unit file +**--new** + +Create a new container via podman-run instead of starting an existing one. This option relies on container configuration files, which may not map directly to podman CLI flags; please review the generated output carefully before placing in production. + **--timeout**, **-t**=*value* Override the default stop timeout for the container with the given value. **--restart-policy**=*policy* + Set the systemd restart policy. The restart-policy must be one of: "no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", or "always". The default policy is *on-failure*. diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index e8744de35..d9fdd9650 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -426,10 +426,21 @@ Logging driver for the container. Currently available options are *k8s-file* an **--log-opt**=*path* -Logging driver specific options. Used to set the path to the container log file. For example: +Set custom logging configuration. Presently supports the `tag` option +which specified a custom log tag for the container. For example: `--log-opt path=/var/log/container/mycontainer.json` +**--log-opt**=*tag* + +Specify a custom log tag for the container. For example: + +`--log-opt tag="{{.ImageName}}"` + +It supports the same keys as `podman inspect --format`. + +It is currently supported only by the journald log driver. + **--mac-address**=*address* Container MAC address (e.g. `92:d0:c6:0a:29:33`) @@ -588,15 +599,16 @@ If a container is run with a pod, and the pod has an infra-container, the infra- Give extended privileges to this container. The default is *false*. -By default, Podman containers are “unprivileged” (=false) and cannot, -for example, modify parts of the kernel. This is because by default a -container is not allowed to access any devices. A “privileged” container -is given access to all devices. +By default, Podman containers are “unprivileged” (=false) and cannot, for +example, modify parts of the operating system. This is because by default a +container is only allowed limited access to devices. A "privileged" container +is given the same access to devices as the user launching the container. -When the operator executes **podman run --privileged**, Podman enables access -to all devices on the host, turns off graphdriver mount options, as well as -turning off most of the security measures protecting the host from the -container. +A privileged container turns off the security features that isolate the +container from the host. Dropped Capabilities, limited devices, read/only mount +points, Apparmor/SELinux separation, and Seccomp filters are all disabled. + +Rootless containers cannot have more privileges than the account that launched them. **--publish**, **-p**=*port* @@ -33,6 +33,10 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf + github.com/google/uuid v1.1.1 + github.com/gorilla/handlers v1.4.2 // indirect + github.com/gorilla/mux v1.7.3 + github.com/gorilla/schema v1.1.0 github.com/hashicorp/go-multierror v1.0.0 github.com/hpcloud/tail v1.0.0 github.com/json-iterator/go v1.1.8 @@ -212,15 +212,20 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v0.0.0-20170217192616-94e7d24fd285/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/libpod/container.go b/libpod/container.go index edf72f4ee..a046dcafc 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -379,6 +379,8 @@ type ContainerConfig struct { CgroupParent string `json:"cgroupParent"` // LogPath log location LogPath string `json:"logPath"` + // LogTag is the tag used for logging + LogTag string `json:"logTag"` // LogDriver driver for logs LogDriver string `json:"logDriver"` // File containing the conmon PID @@ -726,6 +728,11 @@ func (c *Container) LogPath() string { return c.config.LogPath } +// LogTag returns the tag to the container's log file +func (c *Container) LogTag() string { + return c.config.LogTag +} + // RestartPolicy returns the container's restart policy. func (c *Container) RestartPolicy() string { return c.config.RestartPolicy diff --git a/libpod/container_commit.go b/libpod/container_commit.go index a0ba57f4f..ccc23621e 100644 --- a/libpod/container_commit.go +++ b/libpod/container_commit.go @@ -8,6 +8,7 @@ import ( "github.com/containers/buildah" "github.com/containers/buildah/util" is "github.com/containers/image/v5/storage" + "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/image" @@ -32,6 +33,10 @@ type ContainerCommitOptions struct { // Commit commits the changes between a container and its image, creating a new // image func (c *Container) Commit(ctx context.Context, destImage string, options ContainerCommitOptions) (*image.Image, error) { + var ( + imageRef types.ImageReference + ) + if c.config.Rootfs != "" { return nil, errors.Errorf("cannot commit a container that uses an exploded rootfs") } @@ -71,7 +76,6 @@ func (c *Container) Commit(ctx context.Context, destImage string, options Contai if err != nil { return nil, err } - if options.Author != "" { importBuilder.SetMaintainer(options.Author) } @@ -191,12 +195,11 @@ func (c *Container) Commit(ctx context.Context, destImage string, options Contai if err != nil { return nil, errors.Wrapf(err, "error resolving name %q", destImage) } - if len(candidates) == 0 { - return nil, errors.Errorf("error parsing target image name %q", destImage) - } - imageRef, err := is.Transport.ParseStoreReference(c.runtime.store, candidates[0]) - if err != nil { - return nil, errors.Wrapf(err, "error parsing target image name %q", destImage) + if len(candidates) > 0 { + imageRef, err = is.Transport.ParseStoreReference(c.runtime.store, candidates[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing target image name %q", destImage) + } } id, _, _, err := importBuilder.Commit(ctx, imageRef, commitOptions) if err != nil { diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 639dd6e91..3f4ab394f 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -107,6 +107,7 @@ type InspectContainerData struct { OCIConfigPath string `json:"OCIConfigPath,omitempty"` OCIRuntime string `json:"OCIRuntime,omitempty"` LogPath string `json:"LogPath"` + LogTag string `json:"LogTag"` ConmonPidFile string `json:"ConmonPidFile"` Name string `json:"Name"` RestartCount int32 `json:"RestartCount"` @@ -629,17 +630,9 @@ type InspectNetworkSettings struct { MacAddress string `json:"MacAddress"` } -// Inspect a container for low-level information -func (c *Container) Inspect(size bool) (*InspectContainerData, error) { - if !c.batched { - c.lock.Lock() - defer c.lock.Unlock() - - if err := c.syncContainer(); err != nil { - return nil, err - } - } - +// inspectLocked inspects a container for low-level information. +// The caller must held c.lock. +func (c *Container) inspectLocked(size bool) (*InspectContainerData, error) { storeCtr, err := c.runtime.store.Container(c.ID()) if err != nil { return nil, errors.Wrapf(err, "error getting container from store %q", c.ID()) @@ -655,6 +648,20 @@ func (c *Container) Inspect(size bool) (*InspectContainerData, error) { return c.getContainerInspectData(size, driverData) } +// Inspect a container for low-level information +func (c *Container) Inspect(size bool) (*InspectContainerData, error) { + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return nil, err + } + } + + return c.inspectLocked(size) +} + func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) (*InspectContainerData, error) { config := c.config runtimeInfo := c.state @@ -732,6 +739,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) HostsPath: hostsPath, StaticDir: config.StaticDir, LogPath: config.LogPath, + LogTag: config.LogTag, OCIRuntime: config.OCIRuntime, ConmonPidFile: config.ConmonPidFile, Name: config.Name, diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 562f783a7..46c83149a 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -1195,6 +1195,7 @@ func (c *Container) pause() error { } if err := c.ociRuntime.PauseContainer(c); err != nil { + // TODO when using docker-py there is some sort of race/incompatibility here return err } @@ -1212,6 +1213,7 @@ func (c *Container) unpause() error { } if err := c.ociRuntime.UnpauseContainer(c); err != nil { + // TODO when using docker-py there is some sort of race/incompatibility here return err } diff --git a/libpod/image/image.go b/libpod/image/image.go index c8583a1c5..bce10e24c 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -781,6 +781,7 @@ type History struct { CreatedBy string `json:"createdBy"` Size int64 `json:"size"` Comment string `json:"comment"` + Tags []string `json:"tags"` } // History gets the history of an image and the IDs of images that are part of @@ -840,14 +841,17 @@ func (i *Image) History(ctx context.Context) ([]*History, error) { delete(topLayerMap, layer.ID) } } - - allHistory = append(allHistory, &History{ + h := History{ ID: id, Created: oci.History[x].Created, CreatedBy: oci.History[x].CreatedBy, Size: size, Comment: oci.History[x].Comment, - }) + } + if layer != nil { + h.Tags = layer.Names + } + allHistory = append(allHistory, &h) if layer != nil && layer.Parent != "" && !oci.History[x].EmptyLayer { layer, err = i.imageruntime.store.Layer(layer.Parent) diff --git a/libpod/image/prune.go b/libpod/image/prune.go index f5be8ed50..3afff22af 100644 --- a/libpod/image/prune.go +++ b/libpod/image/prune.go @@ -116,6 +116,10 @@ func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ( return nil, errors.Wrap(err, "unable to get images to prune") } for _, p := range pruneImages { + repotags, err := p.RepoTags() + if err != nil { + return nil, err + } if err := p.Remove(ctx, true); err != nil { if errors.Cause(err) == storage.ErrImageUsedByContainer { logrus.Warnf("Failed to prune image %s as it is in use: %v", p.ID(), err) @@ -124,7 +128,11 @@ func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ( return nil, errors.Wrap(err, "failed to prune image") } defer p.newImageEvent(events.Prune) - prunedCids = append(prunedCids, p.ID()) + nameOrID := p.ID() + if len(repotags) > 0 { + nameOrID = repotags[0] + } + prunedCids = append(prunedCids, nameOrID) } return prunedCids, nil } diff --git a/libpod/image/pull.go b/libpod/image/pull.go index 326a23f4c..76294ba06 100644 --- a/libpod/image/pull.go +++ b/libpod/image/pull.go @@ -407,5 +407,5 @@ func checkRemoteImageForLabel(ctx context.Context, label string, imageInfo pullR return nil } } - return errors.Errorf("%s has no label %s", imageInfo.image, label) + return errors.Errorf("%s has no label %s in %q", imageInfo.image, label, remoteInspect.Labels) } diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index b09dc3a4f..3b0b7bc7b 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "syscall" + "text/template" "time" "github.com/containers/libpod/libpod/config" @@ -532,7 +533,7 @@ func (r *ConmonOCIRuntime) ExecContainer(c *Container, sessionID string, options if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON { ociLog = c.execOCILog(sessionID) } - args := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), ociLog) + args := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), ociLog, "") if options.PreserveFDs > 0 { args = append(args, formatRuntimeOpts("--preserve-fds", fmt.Sprintf("%d", options.PreserveFDs))...) @@ -890,6 +891,27 @@ func waitPidStop(pid int, timeout time.Duration) error { } } +func (r *ConmonOCIRuntime) getLogTag(ctr *Container) (string, error) { + logTag := ctr.LogTag() + if logTag == "" { + return "", nil + } + data, err := ctr.inspectLocked(false) + if err != nil { + return "", nil + } + tmpl, err := template.New("container").Parse(logTag) + if err != nil { + return "", errors.Wrapf(err, "template parsing error %s", logTag) + } + var b bytes.Buffer + err = tmpl.Execute(&b, data) + if err != nil { + return "", err + } + return b.String(), nil +} + // createOCIContainer generates this container's main conmon instance and prepares it for starting func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *ContainerCheckpointOptions) (err error) { var stderrBuf bytes.Buffer @@ -916,7 +938,13 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON { ociLog = filepath.Join(ctr.state.RunDir, "oci-log") } - args := r.sharedConmonArgs(ctr, ctr.ID(), ctr.bundlePath(), filepath.Join(ctr.state.RunDir, "pidfile"), ctr.LogPath(), r.exitsDir, ociLog) + + logTag, err := r.getLogTag(ctr) + if err != nil { + return err + } + + args := r.sharedConmonArgs(ctr, ctr.ID(), ctr.bundlePath(), filepath.Join(ctr.state.RunDir, "pidfile"), ctr.LogPath(), r.exitsDir, ociLog, logTag) if ctr.config.Spec.Process.Terminal { args = append(args, "-t") @@ -1150,7 +1178,7 @@ func (r *ConmonOCIRuntime) configureConmonEnv(runtimeDir string) ([]string, []*o } // sharedConmonArgs takes common arguments for exec and create/restore and formats them for the conmon CLI -func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, pidPath, logPath, exitDir, ociLogPath string) []string { +func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, pidPath, logPath, exitDir, ociLogPath, logTag string) []string { // set the conmon API version to be able to use the correct sync struct keys args := []string{"--api-version", "1"} if r.cgroupManager == define.SystemdCgroupsManager && !ctr.config.NoCgroups { @@ -1197,6 +1225,9 @@ func (r *ConmonOCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, p if ociLogPath != "" { args = append(args, "--runtime-arg", "--log-format=json", "--runtime-arg", "--log", fmt.Sprintf("--runtime-arg=%s", ociLogPath)) } + if logTag != "" { + args = append(args, "--log-tag", logTag) + } if ctr.config.NoCgroups { logrus.Debugf("Running with no CGroups") args = append(args, "--runtime-arg", "--cgroup-manager", "--runtime-arg", "disabled") diff --git a/libpod/options.go b/libpod/options.go index 031f4f705..1d6863e7b 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1059,6 +1059,23 @@ func WithLogPath(path string) CtrCreateOption { } } +// WithLogTag sets the tag to the log file. +func WithLogTag(tag string) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + if tag == "" { + return errors.Wrapf(define.ErrInvalidArg, "log tag must be set") + } + + ctr.config.LogTag = tag + + return nil + } + +} + // WithNoCgroups disables the creation of CGroups for the new container. func WithNoCgroups() CtrCreateOption { return func(ctr *Container) error { diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 9943c4104..bae1c1ed8 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -10,6 +10,7 @@ import ( "os" "github.com/containers/buildah/imagebuildah" + "github.com/containers/image/v5/docker/reference" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/util" @@ -145,9 +146,9 @@ func removeStorageContainers(ctrIDs []string, store storage.Store) error { } // Build adds the runtime to the imagebuildah call -func (r *Runtime) Build(ctx context.Context, options imagebuildah.BuildOptions, dockerfiles ...string) error { - _, _, err := imagebuildah.BuildDockerfiles(ctx, r.store, options, dockerfiles...) - return err +func (r *Runtime) Build(ctx context.Context, options imagebuildah.BuildOptions, dockerfiles ...string) (string, reference.Canonical, error) { + id, ref, err := imagebuildah.BuildDockerfiles(ctx, r.store, options, dockerfiles...) + return id, ref, err } // Import is called as an intermediary to the image library Import @@ -192,7 +193,7 @@ func (r *Runtime) Import(ctx context.Context, source string, reference string, c } // if it's stdin, buffer it, too if source == "-" { - file, err := downloadFromFile(os.Stdin) + file, err := DownloadFromFile(os.Stdin) if err != nil { return "", err } @@ -232,9 +233,9 @@ func downloadFromURL(source string) (string, error) { return outFile.Name(), nil } -// donwloadFromFile reads all of the content from the reader and temporarily +// DownloadFromFile reads all of the content from the reader and temporarily // saves in it /var/tmp/importxyz, which is deleted after the image is imported -func downloadFromFile(reader *os.File) (string, error) { +func DownloadFromFile(reader *os.File) (string, error) { outFile, err := ioutil.TempFile("/var/tmp", "import") if err != nil { return "", errors.Wrap(err, "error creating file") diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 3334e9fa1..fdd9f6ab3 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -1230,6 +1230,7 @@ func (r *LocalRuntime) generateSystemdgenContainerInfo(c *cliconfig.GenerateSyst PIDFile: conmonPidFile, StopTimeout: timeout, GenerateTimestamp: true, + CreateCommand: config.CreateCommand, } return info, true, nil @@ -1237,11 +1238,21 @@ func (r *LocalRuntime) generateSystemdgenContainerInfo(c *cliconfig.GenerateSyst // GenerateSystemd creates a unit file for a container or pod. func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) { + opts := systemdgen.Options{ + Files: c.Files, + New: c.New, + } + // First assume it's a container. if info, found, err := r.generateSystemdgenContainerInfo(c, c.InputArgs[0], nil); found && err != nil { return "", err } else if found && err == nil { - return systemdgen.CreateContainerSystemdUnit(info, c.Files) + return systemdgen.CreateContainerSystemdUnit(info, opts) + } + + // --new does not support pods. + if c.New { + return "", errors.Errorf("error generating systemd unit files: cannot generate generic files for a pod") } // We're either having a pod or garbage. @@ -1312,7 +1323,7 @@ func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (stri if i > 0 { builder.WriteByte('\n') } - out, err := systemdgen.CreateContainerSystemdUnit(info, c.Files) + out, err := systemdgen.CreateContainerSystemdUnit(info, opts) if err != nil { return "", err } diff --git a/pkg/adapter/images.go b/pkg/adapter/images.go index c8ea1cdea..762f1a656 100644 --- a/pkg/adapter/images.go +++ b/pkg/adapter/images.go @@ -3,14 +3,13 @@ package adapter import ( - "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/libpod/image" "github.com/pkg/errors" ) // Tree ... -func (r *LocalRuntime) Tree(c *cliconfig.TreeValues) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { - img, err := r.NewImageFromLocal(c.InputArgs[0]) +func (r *LocalRuntime) Tree(imageOrID string) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { + img, err := r.NewImageFromLocal(imageOrID) if err != nil { return nil, nil, nil, err } diff --git a/pkg/adapter/images_remote.go b/pkg/adapter/images_remote.go index 722058d4a..1d4997d9a 100644 --- a/pkg/adapter/images_remote.go +++ b/pkg/adapter/images_remote.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" - "github.com/containers/libpod/cmd/podman/cliconfig" iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/inspect" @@ -27,11 +26,11 @@ func (i *ContainerImage) Inspect(ctx context.Context) (*inspect.ImageData, error } // Tree ... -func (r *LocalRuntime) Tree(c *cliconfig.TreeValues) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { +func (r *LocalRuntime) Tree(imageOrID string) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { layerInfoMap := make(map[string]*image.LayerInfo) imageInfo := &image.InfoImage{} - img, err := r.NewImageFromLocal(c.InputArgs[0]) + img, err := r.NewImageFromLocal(imageOrID) if err != nil { return nil, nil, nil, err } @@ -44,7 +43,7 @@ func (r *LocalRuntime) Tree(c *cliconfig.TreeValues) (*image.InfoImage, map[stri return nil, nil, nil, errors.Wrap(err, "failed to unmarshal image layers") } - reply, err = iopodman.BuildImageHierarchyMap().Call(r.Conn, c.InputArgs[0]) + reply, err = iopodman.BuildImageHierarchyMap().Call(r.Conn, imageOrID) if err != nil { return nil, nil, nil, errors.Wrap(err, "failed to get build image map") } diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index 8933e826f..5f880e807 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -286,20 +286,20 @@ 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) error { +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 errors.Wrapf(err, "error parsing namespace-related options") + return "", nil, errors.Wrapf(err, "error parsing namespace-related options") } usernsOption, idmappingOptions, err := parse.IDMappingOptions(c.PodmanCommand.Command, options.Isolation) if err != nil { - return errors.Wrapf(err, "error parsing ID mapping options") + return "", nil, errors.Wrapf(err, "error parsing ID mapping options") } namespaceOptions.AddOrReplace(usernsOption...) systemContext, err := parse.SystemContextFromOptions(c.PodmanCommand.Command) if err != nil { - return errors.Wrapf(err, "error building system context") + return "", nil, errors.Wrapf(err, "error building system context") } authfile := c.Authfile @@ -310,7 +310,7 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti systemContext.AuthFilePath = authfile commonOpts, err := parse.CommonBuildOptions(c.PodmanCommand.Command) if err != nil { - return err + return "", nil, err } options.NamespaceOptions = namespaceOptions diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index 9c10b31c0..c908358ff 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -507,7 +507,7 @@ func (r *LocalRuntime) Import(ctx context.Context, source, reference string, cha return iopodman.ImportImage().Call(r.Conn, strings.TrimRight(tempFile, ":"), reference, history, changes, true) } -func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, options imagebuildah.BuildOptions, dockerfiles []string) error { +func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, options imagebuildah.BuildOptions, dockerfiles []string) (string, reference.Canonical, error) { buildOptions := iopodman.BuildOptions{ AddHosts: options.CommonBuildOpts.AddHost, CgroupParent: options.CommonBuildOpts.CgroupParent, @@ -552,31 +552,31 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti // tar the file outputFile, err := ioutil.TempFile("", "varlink_tar_send") if err != nil { - return err + return "", nil, err } defer outputFile.Close() defer os.Remove(outputFile.Name()) // Create the tarball of the context dir to a tempfile if err := utils.TarToFilesystem(options.ContextDirectory, outputFile); err != nil { - return err + return "", nil, err } // Send the context dir tarball over varlink. tempFile, err := r.SendFileOverVarlink(outputFile.Name()) if err != nil { - return err + return "", nil, err } buildinfo.ContextDir = tempFile reply, err := iopodman.BuildImage().Send(r.Conn, varlink.More, buildinfo) if err != nil { - return err + return "", nil, err } for { responses, flags, err := reply() if err != nil { - return err + return "", nil, err } for _, line := range responses.Logs { fmt.Print(line) @@ -585,7 +585,7 @@ func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, opti break } } - return err + return "", nil, err } // SendFileOverVarlink sends a file over varlink in an upgraded connection diff --git a/pkg/api/handlers/containers.go b/pkg/api/handlers/containers.go new file mode 100644 index 000000000..6b09321a0 --- /dev/null +++ b/pkg/api/handlers/containers.go @@ -0,0 +1,194 @@ +package handlers + +import ( + "fmt" + "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 := 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 302 + if state == define.ContainerStateStopped || state == define.ContainerStateExited { + utils.Error(w, http.StatusText(http.StatusNotModified), http.StatusNotModified, + errors.Errorf("Container %s is already stopped ", name)) + 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 := 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 := 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 := 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 { + msg := fmt.Sprintf("Container %s is already running", name) + utils.Error(w, msg, http.StatusNotModified, errors.New(msg)) + 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 := 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, "") +} diff --git a/pkg/api/handlers/containers_top.go b/pkg/api/handlers/containers_top.go new file mode 100644 index 000000000..bab559da1 --- /dev/null +++ b/pkg/api/handlers/containers_top.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "net/http" + "strings" + + "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 TopContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + PsArgs string `schema:"ps_args"` + }{ + PsArgs: "-ef", + } + 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 := mux.Vars(r)["name"] + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := ctnr.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + if state != define.ContainerStateRunning { + utils.ContainerNotRunning(w, name, errors.Errorf("Container %s must be running to perform top operation", name)) + return + } + + output, err := ctnr.Top([]string{}) + if err != nil { + utils.InternalServerError(w, err) + return + } + + var body = ContainerTopOKBody{} + if len(output) > 0 { + body.Titles = strings.Split(output[0], "\t") + for _, line := range output[1:] { + body.Processes = append(body.Processes, strings.Split(line, "\t")) + } + } + utils.WriteJSON(w, http.StatusOK, body) +} diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/events.go new file mode 100644 index 000000000..900efa3da --- /dev/null +++ b/pkg/api/handlers/events.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func GetEvents(w http.ResponseWriter, r *http.Request) { + query := struct { + Since string `json:"since"` + Until string `json:"until"` + Filters string `json:"filters"` + }{} + if err := decodeQuery(r, &query); err != nil { + utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + } + + var filters = map[string][]string{} + if found := hasVar(r, "filters"); found { + if err := json.Unmarshal([]byte(query.Filters), &filters); err != nil { + utils.BadRequest(w, "filters", query.Filters, err) + return + } + } + + var libpodFilters = make([]string, len(filters)) + for k, v := range filters { + libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) + } + + libpodEvents, err := getRuntime(r).GetEvents(libpodFilters) + if err != nil { + utils.BadRequest(w, "filters", query.Filters, err) + return + } + + var apiEvents = make([]*Event, len(libpodEvents)) + for _, v := range libpodEvents { + apiEvents = append(apiEvents, EventToApiEvent(v)) + } + utils.WriteJSON(w, http.StatusOK, apiEvents) +} diff --git a/pkg/api/handlers/generic/containers.go b/pkg/api/handlers/generic/containers.go new file mode 100644 index 000000000..5a0a51fd7 --- /dev/null +++ b/pkg/api/handlers/generic/containers.go @@ -0,0 +1,306 @@ +package generic + +import ( + "context" + "encoding/binary" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "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/util" + "github.com/docker/docker/api/types" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +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"` + Link bool `schema:"link"` + }{ + // 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 + } + if query.Link { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + utils.ErrLinkNotSupport) + return + } + utils.RemoveContainer(w, r, query.Force, query.Vols) +} + +func ListContainers(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + containers, err := runtime.GetAllContainers() + if err != nil { + utils.InternalServerError(w, err) + return + } + + infoData, err := runtime.Info() + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "Failed to obtain system info")) + return + } + + var list = make([]*handlers.Container, len(containers)) + for i, ctnr := range containers { + api, err := handlers.LibpodToContainer(ctnr, infoData) + if err != nil { + utils.InternalServerError(w, err) + return + } + list[i] = api + } + utils.WriteResponse(w, http.StatusOK, list) +} + +func GetContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + name := mux.Vars(r)["name"] + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + api, err := handlers.LibpodToContainerJSON(ctnr) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, api) +} + +func KillContainer(w http.ResponseWriter, r *http.Request) { + // /{version}/containers/(name)/kill + con, err := utils.KillContainer(w, r) + if err != nil { + 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())) + return + } + // Success + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func WaitContainer(w http.ResponseWriter, r *http.Request) { + var msg string + // /{version}/containers/(name)/wait + exitCode, err := utils.WaitContainer(w, r) + if err != nil { + msg = err.Error() + } + utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{ + StatusCode: int(exitCode), + Error: struct { + Message string + }{ + Message: msg, + }, + }) +} + +func PruneContainers(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + containers, err := runtime.GetAllContainers() + if err != nil { + utils.InternalServerError(w, err) + return + } + + deletedContainers := []string{} + var spaceReclaimed uint64 + for _, ctnr := range containers { + // Only remove stopped or exit'ed containers. + state, err := ctnr.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + switch state { + case define.ContainerStateStopped, define.ContainerStateExited: + default: + continue + } + + deletedContainers = append(deletedContainers, ctnr.ID()) + cSize, err := ctnr.RootFsSize() + if err != nil { + utils.InternalServerError(w, err) + return + } + spaceReclaimed += uint64(cSize) + + err = runtime.RemoveContainer(context.Background(), ctnr, false, false) + if err != nil && !(errors.Cause(err) == define.ErrCtrRemoved) { + utils.InternalServerError(w, err) + return + } + } + report := types.ContainersPruneReport{ + ContainersDeleted: deletedContainers, + SpaceReclaimed: spaceReclaimed, + } + utils.WriteResponse(w, http.StatusOK, report) +} + +func LogsFromContainer(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Follow bool `schema:"follow"` + Stdout bool `schema:"stdout"` + Stderr bool `schema:"stderr"` + Since string `schema:"since"` + Until string `schema:"until"` + Timestamps bool `schema:"timestamps"` + Tail string `schema:"tail"` + }{ + Tail: "all", + } + 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 !(query.Stdout || query.Stderr) { + msg := fmt.Sprintf("%s: you must choose at least one stream", http.StatusText(http.StatusBadRequest)) + utils.Error(w, msg, http.StatusBadRequest, errors.Errorf("%s for %s", msg, r.URL.String())) + return + } + + name := mux.Vars(r)["name"] + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + var tail int64 = -1 + if query.Tail != "all" { + tail, err = strconv.ParseInt(query.Tail, 0, 64) + if err != nil { + utils.BadRequest(w, "tail", query.Tail, err) + return + } + } + + var since time.Time + if _, found := mux.Vars(r)["since"]; found { + since, err = util.ParseInputTime(query.Since) + if err != nil { + utils.BadRequest(w, "since", query.Since, err) + return + } + } + + var until time.Time + if _, found := mux.Vars(r)["until"]; found { + since, err = util.ParseInputTime(query.Until) + if err != nil { + utils.BadRequest(w, "until", query.Until, err) + return + } + } + + options := &logs.LogOptions{ + Details: true, + Follow: query.Follow, + Since: since, + Tail: tail, + Timestamps: query.Timestamps, + } + + var wg sync.WaitGroup + options.WaitGroup = &wg + + logChannel := make(chan *logs.LogLine, tail+1) + if err := runtime.Log([]*libpod.Container{ctnr}, options, logChannel); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "Failed to obtain logs for Container '%s'", name)) + return + } + go func() { + wg.Wait() + close(logChannel) + }() + + w.WriteHeader(http.StatusOK) + var builder strings.Builder + for ok := true; ok; ok = query.Follow { + for line := range logChannel { + if _, found := mux.Vars(r)["until"]; found { + if line.Time.After(until) { + break + } + } + + // Reset variables we're ready to loop again + builder.Reset() + header := [8]byte{} + + switch line.Device { + case "stdout": + if !query.Stdout { + continue + } + header[0] = 1 + case "stderr": + if !query.Stderr { + continue + } + header[0] = 2 + default: + // Logging and moving on is the best we can do here. We may have already sent + // a Status and Content-Type to client therefore we can no longer report an error. + log.Infof("unknown Device type '%s' in log file from Container %s", line.Device, ctnr.ID()) + continue + } + + if query.Timestamps { + builder.WriteString(line.Time.Format(time.RFC3339)) + 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 { + log.Errorf("unable to write log output header: %q", err) + } + 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/generic/containers_create.go b/pkg/api/handlers/generic/containers_create.go new file mode 100644 index 000000000..ef5337abd --- /dev/null +++ b/pkg/api/handlers/generic/containers_create.go @@ -0,0 +1,243 @@ +package generic + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/containers/libpod/cmd/podman/shared" + "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" + "github.com/containers/libpod/pkg/namespaces" + createconfig "github.com/containers/libpod/pkg/spec" + "github.com/containers/storage" + "github.com/docker/docker/pkg/signal" + "github.com/gorilla/schema" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +func CreateContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + input := handlers.CreateContainerConfig{} + query := struct { + Name string `schema:"name"` + }{ + // 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 + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + + newImage, err := runtime.ImageRuntime().NewFromLocal(input.Image) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "NewFromLocal()")) + return + } + cc, err := makeCreateConfig(input, newImage) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) + return + } + + cc.Name = query.Name + var pod *libpod.Pod + ctr, err := shared.CreateContainerFromCreateConfig(runtime, &cc, r.Context(), pod) + if err != nil { + if strings.Contains(err.Error(), "invalid log driver") { + // this does not quite work yet and needs a little more massaging + w.Header().Set("Content-Type", "text/plain; charset=us-ascii") + w.WriteHeader(http.StatusInternalServerError) + msg := fmt.Sprintf("logger: no log driver named '%s' is registered", input.HostConfig.LogConfig.Type) + if _, err := fmt.Fprintln(w, msg); err != nil { + log.Errorf("%s: %q", msg, err) + } + //s.WriteResponse(w, http.StatusInternalServerError, fmt.Sprintf("logger: no log driver named '%s' is registered", input.HostConfig.LogConfig.Type)) + return + } + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "CreateContainerFromCreateConfig()")) + return + } + + type ctrCreateResponse struct { + Id string `json:"Id"` + Warnings []string `json:"Warnings"` + } + response := ctrCreateResponse{ + Id: ctr.ID(), + Warnings: []string{}} + + utils.WriteResponse(w, http.StatusCreated, response) +} + +func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { + var ( + err error + init bool + tmpfs []string + volumes []string + ) + env := make(map[string]string) + stopSignal := unix.SIGTERM + if len(input.StopSignal) > 0 { + stopSignal, err = signal.ParseSignal(input.StopSignal) + if err != nil { + return createconfig.CreateConfig{}, err + } + } + + workDir := "/" + if len(input.WorkingDir) > 0 { + workDir = input.WorkingDir + } + + stopTimeout := uint(define.CtrRemoveTimeout) + if input.StopTimeout != nil { + stopTimeout = uint(*input.StopTimeout) + } + c := createconfig.CgroupConfig{ + Cgroups: "", // podman + Cgroupns: "", // podman + CgroupParent: "", // podman + CgroupMode: "", // podman + } + security := createconfig.SecurityConfig{ + CapAdd: input.HostConfig.CapAdd, + CapDrop: input.HostConfig.CapDrop, + LabelOpts: nil, // podman + NoNewPrivs: false, // podman + ApparmorProfile: "", // podman + SeccompProfilePath: "", + SecurityOpts: input.HostConfig.SecurityOpt, + Privileged: input.HostConfig.Privileged, + ReadOnlyRootfs: input.HostConfig.ReadonlyRootfs, + ReadOnlyTmpfs: false, // podman-only + Sysctl: input.HostConfig.Sysctls, + } + + network := createconfig.NetworkConfig{ + DNSOpt: input.HostConfig.DNSOptions, + DNSSearch: input.HostConfig.DNSSearch, + DNSServers: input.HostConfig.DNS, + ExposedPorts: input.ExposedPorts, + HTTPProxy: false, // podman + IP6Address: "", + IPAddress: "", + LinkLocalIP: nil, // docker-only + MacAddress: input.MacAddress, + // NetMode: nil, + Network: input.HostConfig.NetworkMode.NetworkName(), + NetworkAlias: nil, // docker-only now + PortBindings: input.HostConfig.PortBindings, + Publish: nil, // podmanseccompPath + PublishAll: input.HostConfig.PublishAllPorts, + } + + uts := createconfig.UtsConfig{ + UtsMode: namespaces.UTSMode(input.HostConfig.UTSMode), + NoHosts: false, //podman + HostAdd: input.HostConfig.ExtraHosts, + Hostname: input.Hostname, + } + + z := createconfig.UserConfig{ + GroupAdd: input.HostConfig.GroupAdd, + IDMappings: &storage.IDMappingOptions{}, // podman //TODO <--- fix this, + UsernsMode: namespaces.UsernsMode(input.HostConfig.UsernsMode), + User: input.User, + } + pidConfig := createconfig.PidConfig{PidMode: namespaces.PidMode(input.HostConfig.PidMode)} + for k := range input.Volumes { + volumes = append(volumes, k) + } + + // Docker is more flexible about its input where podman throws + // away incorrectly formatted variables so we cannot reuse the + // parsing of the env input + // [Foo Other=one Blank=] + for _, e := range input.Env { + splitEnv := strings.Split(e, "=") + switch len(splitEnv) { + case 0: + continue + case 1: + env[splitEnv[0]] = "" + default: + env[splitEnv[0]] = strings.Join(splitEnv[1:], "=") + } + } + + // format the tmpfs mounts into a []string from map + for k, v := range input.HostConfig.Tmpfs { + tmpfs = append(tmpfs, fmt.Sprintf("%s:%s", k, v)) + } + + if input.HostConfig.Init != nil && *input.HostConfig.Init { + init = true + } + + m := createconfig.CreateConfig{ + Annotations: nil, // podman + Args: nil, + Cgroup: c, + CidFile: "", + ConmonPidFile: "", // podman + Command: input.Cmd, + UserCommand: input.Cmd, // podman + Detach: false, // + // Devices: input.HostConfig.Devices, + Entrypoint: input.Entrypoint, + Env: env, + HealthCheck: nil, // + Init: init, + InitPath: "", // tbd + Image: input.Image, + ImageID: newImage.ID(), + BuiltinImgVolumes: nil, // podman + ImageVolumeType: "", // podman + Interactive: false, + // IpcMode: input.HostConfig.IpcMode, + Labels: input.Labels, + LogDriver: input.HostConfig.LogConfig.Type, // is this correct + // LogDriverOpt: input.HostConfig.LogConfig.Config, + Name: input.Name, + Network: network, + Pod: "", // podman + PodmanPath: "", // podman + Quiet: false, // front-end only + Resources: createconfig.CreateResourceConfig{}, + RestartPolicy: input.HostConfig.RestartPolicy.Name, + Rm: input.HostConfig.AutoRemove, + StopSignal: stopSignal, + StopTimeout: stopTimeout, + Systemd: false, // podman + Tmpfs: tmpfs, + User: z, + Uts: uts, + Tty: input.Tty, + Mounts: nil, // we populate + // MountsFlag: input.HostConfig.Mounts, + NamedVolumes: nil, // we populate + Volumes: volumes, + VolumesFrom: input.HostConfig.VolumesFrom, + WorkDir: workDir, + Rootfs: "", // podman + Security: security, + Syslog: false, // podman + + Pid: pidConfig, + } + return m, nil +} diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/generic/containers_stats.go new file mode 100644 index 000000000..0c4efc1df --- /dev/null +++ b/pkg/api/handlers/generic/containers_stats.go @@ -0,0 +1,198 @@ +package generic + +import ( + "encoding/json" + "net/http" + "time" + + "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/cgroups" + docker "github.com/docker/docker/api/types" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const DefaultStatsPeriod = 5 * time.Second + +func StatsContainer(w http.ResponseWriter, r *http.Request) { + // 200 no error + // 404 no such + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + Stream bool `schema:"stream"` + }{ + Stream: true, + } + 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 := mux.Vars(r)["name"] + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := ctnr.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + if state != define.ContainerStateRunning && !query.Stream { + utils.WriteJSON(w, http.StatusOK, &handlers.Stats{StatsJSON: docker.StatsJSON{ + Name: ctnr.Name(), + ID: ctnr.ID(), + }}) + return + } + + var preRead time.Time + var preCPUStats docker.CPUStats + + stats, err := ctnr.GetContainerStats(&libpod.ContainerStats{}) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "Failed to obtain Container %s stats", name)) + return + } + + if query.Stream { + preRead = time.Now() + preCPUStats = docker.CPUStats{ + CPUUsage: docker.CPUUsage{ + TotalUsage: stats.CPUNano, + PercpuUsage: []uint64{uint64(stats.CPU)}, + UsageInKernelmode: 0, + UsageInUsermode: 0, + }, + SystemUsage: 0, + OnlineCPUs: 0, + ThrottlingData: docker.ThrottlingData{}, + } + time.Sleep(DefaultStatsPeriod) + } + + cgroupPath, _ := ctnr.CGroupPath() + cgroup, _ := cgroups.Load(cgroupPath) + + for ok := true; ok; ok = query.Stream { + state, _ := ctnr.State() + if state != define.ContainerStateRunning { + time.Sleep(10 * time.Second) + continue + } + + stats, _ := ctnr.GetContainerStats(stats) + cgroupStat, _ := cgroup.Stat() + inspect, _ := ctnr.Inspect(false) + + net := make(map[string]docker.NetworkStats) + net[inspect.NetworkSettings.EndpointID] = docker.NetworkStats{ + RxBytes: stats.NetInput, + RxPackets: 0, + RxErrors: 0, + RxDropped: 0, + TxBytes: stats.NetOutput, + TxPackets: 0, + TxErrors: 0, + TxDropped: 0, + EndpointID: inspect.NetworkSettings.EndpointID, + InstanceID: "", + } + + s := handlers.Stats{StatsJSON: docker.StatsJSON{ + Stats: docker.Stats{ + Read: time.Now(), + PreRead: preRead, + PidsStats: docker.PidsStats{ + Current: cgroupStat.Pids.Current, + Limit: 0, + }, + BlkioStats: docker.BlkioStats{ + IoServiceBytesRecursive: toBlkioStatEntry(cgroupStat.Blkio.IoServiceBytesRecursive), + IoServicedRecursive: nil, + IoQueuedRecursive: nil, + IoServiceTimeRecursive: nil, + IoWaitTimeRecursive: nil, + IoMergedRecursive: nil, + IoTimeRecursive: nil, + SectorsRecursive: nil, + }, + NumProcs: 0, + StorageStats: docker.StorageStats{ + ReadCountNormalized: 0, + ReadSizeBytes: 0, + WriteCountNormalized: 0, + WriteSizeBytes: 0, + }, + CPUStats: docker.CPUStats{ + CPUUsage: docker.CPUUsage{ + TotalUsage: cgroupStat.CPU.Usage.Total, + PercpuUsage: []uint64{uint64(stats.CPU)}, + UsageInKernelmode: cgroupStat.CPU.Usage.Kernel, + UsageInUsermode: cgroupStat.CPU.Usage.Total - cgroupStat.CPU.Usage.Kernel, + }, + SystemUsage: 0, + OnlineCPUs: uint32(len(cgroupStat.CPU.Usage.PerCPU)), + ThrottlingData: docker.ThrottlingData{ + Periods: 0, + ThrottledPeriods: 0, + ThrottledTime: 0, + }, + }, + PreCPUStats: preCPUStats, + MemoryStats: docker.MemoryStats{ + Usage: cgroupStat.Memory.Usage.Usage, + MaxUsage: cgroupStat.Memory.Usage.Limit, + Stats: nil, + Failcnt: 0, + Limit: cgroupStat.Memory.Usage.Limit, + Commit: 0, + CommitPeak: 0, + PrivateWorkingSet: 0, + }, + }, + Name: stats.Name, + ID: stats.ContainerID, + Networks: net, + }} + + utils.WriteJSON(w, http.StatusOK, s) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + preRead = s.Read + bits, err := json.Marshal(s.CPUStats) + if err != nil { + logrus.Errorf("unable to marshal cpu stats: %q", err) + } + if err := json.Unmarshal(bits, &preCPUStats); err != nil { + logrus.Errorf("unable to unmarshal previous stats: %q", err) + } + time.Sleep(DefaultStatsPeriod) + } +} + +func toBlkioStatEntry(entries []cgroups.BlkIOEntry) []docker.BlkioStatEntry { + results := make([]docker.BlkioStatEntry, 0, len(entries)) + for i, e := range entries { + bits, err := json.Marshal(e) + if err != nil { + logrus.Errorf("unable to marshal blkio stats: %q", err) + } + if err := json.Unmarshal(bits, &results[i]); err != nil { + logrus.Errorf("unable to unmarshal blkio stats: %q", err) + } + } + return results +} diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/generic/images.go new file mode 100644 index 000000000..8029ee861 --- /dev/null +++ b/pkg/api/handlers/generic/images.go @@ -0,0 +1,363 @@ +package generic + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + + "github.com/containers/buildah" + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/libpod" + 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/util" + "github.com/containers/storage" + "github.com/docker/docker/api/types" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func ExportImage(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 server + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + name := mux.Vars(r)["name"] + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + tmpfile, err := ioutil.TempFile("", "api.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + if err := tmpfile.Close(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + return + } + if err := newImage.Save(r.Context(), name, "docker-archive", tmpfile.Name(), []string{}, false, false); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to save image")) + return + } + rdr, err := os.Open(tmpfile.Name()) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) + return + } + defer rdr.Close() + defer os.Remove(tmpfile.Name()) + utils.WriteResponse(w, http.StatusOK, rdr) +} + +func PruneImages(w http.ResponseWriter, r *http.Request) { + // 200 no error + // 500 internal + var ( + dangling bool = true + err error + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + filters map[string]string + }{ + // 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 + } + + // FIXME This is likely wrong due to it not being a map[string][]string + + // until ts is not supported on podman prune + if len(query.filters["until"]) > 0 { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "until is not supported yet")) + return + } + // labels are not supported on podman prune + if len(query.filters["label"]) > 0 { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "labelis not supported yet")) + return + } + + if len(query.filters["dangling"]) > 0 { + dangling, err = strconv.ParseBool(query.filters["dangling"]) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "processing dangling filter")) + return + } + } + idr := []types.ImageDeleteResponseItem{} + // + // This code needs to be migrated to libpod to work correctly. I could not + // work my around the information docker needs with the existing prune in libpod. + // + pruneImages, err := runtime.ImageRuntime().GetPruneImages(!dangling, []image2.ImageFilter{}) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get images to prune")) + return + } + for _, p := range pruneImages { + repotags, err := p.RepoTags() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get repotags for image")) + return + } + if err := p.Remove(r.Context(), true); err != nil { + if errors.Cause(err) == storage.ErrImageUsedByContainer { + logrus.Warnf("Failed to prune image %s as it is in use: %v", p.ID(), err) + continue + } + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to prune image")) + return + } + // newimageevent is not export therefore we cannot record the event. this will be fixed + // when the prune is fixed in libpod + // defer p.newImageEvent(events.Prune) + response := types.ImageDeleteResponseItem{ + Deleted: fmt.Sprintf("sha256:%s", p.ID()), // I ack this is not ideal + } + if len(repotags) > 0 { + response.Untagged = repotags[0] + } + idr = append(idr, response) + } + ipr := types.ImagesPruneReport{ + ImagesDeleted: idr, + SpaceReclaimed: 1, // TODO we cannot supply this right now + } + utils.WriteResponse(w, http.StatusOK, handlers.ImagesPruneReport{ImagesPruneReport: ipr}) +} + +func CommitContainer(w http.ResponseWriter, r *http.Request) { + var ( + destImage string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + author string + changes string + comment string + container string + //fromSrc string # fromSrc is currently unused + pause bool + repo string + tag string + }{ + // 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 + } + rtc, err := runtime.GetConfig() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + sc := image2.GetSystemContext(rtc.SignaturePolicyPath, "", false) + tag := "latest" + options := libpod.ContainerCommitOptions{ + Pause: true, + } + options.CommitOptions = buildah.CommitOptions{ + SignaturePolicyPath: rtc.SignaturePolicyPath, + ReportWriter: os.Stderr, + SystemContext: sc, + PreferredManifestType: manifest.DockerV2Schema2MediaType, + } + + input := handlers.CreateContainerConfig{} + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + + if len(query.tag) > 0 { + tag = query.tag + } + options.Message = query.comment + options.Author = query.author + options.Pause = query.pause + options.Changes = strings.Fields(query.changes) + ctr, err := runtime.LookupContainer(query.container) + if err != nil { + utils.Error(w, "Something went wrong.", 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 +} + +func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { + // 200 no error + // 404 repo does not exist or no read access + // 500 internal + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + fromSrc string + changes []string + }{ + // 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 + } + // fromSrc – Source to import. The value may be a URL from which the image can be retrieved or - to read the image from the request body. This parameter may only be used when importing an image. + source := query.fromSrc + if source == "-" { + f, err := ioutil.TempFile("", "api_load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) + return + } + source = f.Name() + if err := handlers.SaveFromBody(f, r); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) + } + } + iid, err := runtime.Import(r.Context(), source, "", query.changes, "", false) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to import tarball")) + return + } + tmpfile, err := ioutil.TempFile("", "fromsrc.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + if err := tmpfile.Close(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + return + } + // Success + utils.WriteResponse(w, http.StatusOK, struct { + Status string `json:"status"` + Progress string `json:"progress"` + ProgressDetail map[string]string `json:"progressDetail"` + Id string `json:"id"` + }{ + Status: iid, + ProgressDetail: map[string]string{}, + Id: iid, + }) + +} + +func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { + // 200 no error + // 404 repo does not exist or no read access + // 500 internal + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + fromImage string + tag string + }{ + // 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 + } + + /* + fromImage – Name of the image to pull. The name may include a tag or digest. This parameter may only be used when pulling an image. The pull is cancelled if the HTTP connection is closed. + repo – Repository name given to an image when it is imported. The repo may include a tag. This parameter may only be used when importing an image. + tag – Tag or digest. If empty when pulling an image, this causes all tags for the given image to be pulled. + */ + fromImage := query.fromImage + if len(query.tag) < 1 { + fromImage = fmt.Sprintf("%s:%s", fromImage, query.tag) + } + + // TODO + // We are eating the output right now because we haven't talked about how to deal with multiple responses yet + img, err := runtime.ImageRuntime().New(r.Context(), fromImage, "", "", nil, &image2.DockerRegistryOptions{}, image2.SigningOptions{}, nil, util.PullImageMissing) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + // Success + utils.WriteResponse(w, http.StatusOK, struct { + Status string `json:"status"` + Error string `json:"error"` + Progress string `json:"progress"` + ProgressDetail map[string]string `json:"progressDetail"` + Id string `json:"id"` + }{ + Status: fmt.Sprintf("pulling image (%s) from %s", img.Tag, strings.Join(img.Names(), ", ")), + ProgressDetail: map[string]string{}, + Id: img.ID(), + }) +} + +func GetImage(w http.ResponseWriter, r *http.Request) { + // 200 no error + // 404 no such + // 500 internal + name := mux.Vars(r)["name"] + newImage, err := handlers.GetImage(r, name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + inspect, err := handlers.ImageDataToImageInspect(r.Context(), newImage) + if err != nil { + utils.Error(w, "Server error", http.StatusInternalServerError, errors.Wrapf(err, "Failed to convert ImageData to ImageInspect '%s'", inspect.ID)) + return + } + utils.WriteResponse(w, http.StatusOK, inspect) +} + +func GetImages(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + images, err := utils.GetImages(w, r) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) + return + } + var summaries = make([]*handlers.ImageSummary, len(images)) + for j, img := range images { + is, err := handlers.ImageToImageSummary(img) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed transform image summaries")) + return + } + summaries[j] = is + } + utils.WriteResponse(w, http.StatusOK, summaries) +} diff --git a/pkg/api/handlers/generic/info.go b/pkg/api/handlers/generic/info.go new file mode 100644 index 000000000..c9e79233d --- /dev/null +++ b/pkg/api/handlers/generic/info.go @@ -0,0 +1,196 @@ +package generic + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + goRuntime "runtime" + "strings" + "time" + + "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" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/sysinfo" + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/google/uuid" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func GetInfo(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + infoData, err := runtime.Info() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to obtain system memory info")) + return + } + hostInfo := infoData[0].Data + storeInfo := infoData[1].Data + + configInfo, err := runtime.GetConfig() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to obtain runtime config")) + return + } + versionInfo, err := define.GetVersion() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to obtain podman versions")) + return + } + stateInfo := getContainersState(runtime) + sysInfo := sysinfo.New(true) + + // FIXME: Need to expose if runtime supports Checkpoint'ing + // liveRestoreEnabled := criu.CheckForCriu() && configInfo.RuntimeSupportsCheckpoint() + + info := &handlers.Info{Info: docker.Info{ + Architecture: goRuntime.GOARCH, + BridgeNfIP6tables: !sysInfo.BridgeNFCallIP6TablesDisabled, + BridgeNfIptables: !sysInfo.BridgeNFCallIPTablesDisabled, + CPUCfsPeriod: sysInfo.CPUCfsPeriod, + CPUCfsQuota: sysInfo.CPUCfsQuota, + CPUSet: sysInfo.Cpuset, + CPUShares: sysInfo.CPUShares, + CgroupDriver: configInfo.CgroupManager, + ClusterAdvertise: "", + ClusterStore: "", + ContainerdCommit: docker.Commit{}, + Containers: storeInfo["ContainerStore"].(map[string]interface{})["number"].(int), + ContainersPaused: stateInfo[define.ContainerStatePaused], + ContainersRunning: stateInfo[define.ContainerStateRunning], + ContainersStopped: stateInfo[define.ContainerStateStopped] + stateInfo[define.ContainerStateExited], + Debug: log.IsLevelEnabled(log.DebugLevel), + DefaultRuntime: configInfo.OCIRuntime, + DockerRootDir: storeInfo["GraphRoot"].(string), + Driver: storeInfo["GraphDriverName"].(string), + DriverStatus: getGraphStatus(storeInfo), + ExperimentalBuild: true, + GenericResources: nil, + HTTPProxy: getEnv("http_proxy"), + HTTPSProxy: getEnv("https_proxy"), + ID: uuid.New().String(), + IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled, + Images: storeInfo["ImageStore"].(map[string]interface{})["number"].(int), + IndexServerAddress: "", + InitBinary: "", + InitCommit: docker.Commit{}, + Isolation: "", + KernelMemory: sysInfo.KernelMemory, + KernelMemoryTCP: false, + KernelVersion: hostInfo["kernel"].(string), + Labels: nil, + LiveRestoreEnabled: false, + LoggingDriver: "", + MemTotal: hostInfo["MemTotal"].(int64), + MemoryLimit: sysInfo.MemoryLimit, + NCPU: goRuntime.NumCPU(), + NEventsListener: 0, + NFd: getFdCount(), + NGoroutines: goRuntime.NumGoroutine(), + Name: hostInfo["hostname"].(string), + NoProxy: getEnv("no_proxy"), + OSType: goRuntime.GOOS, + OSVersion: hostInfo["Distribution"].(map[string]interface{})["version"].(string), + OomKillDisable: sysInfo.OomKillDisable, + OperatingSystem: hostInfo["Distribution"].(map[string]interface{})["distribution"].(string), + PidsLimit: sysInfo.PidsLimit, + Plugins: docker.PluginsInfo{}, + ProductLicense: "Apache-2.0", + RegistryConfig: nil, + RuncCommit: docker.Commit{}, + Runtimes: getRuntimes(configInfo), + SecurityOptions: getSecOpts(sysInfo), + ServerVersion: versionInfo.Version, + SwapLimit: sysInfo.SwapLimit, + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateInactive, + }, + SystemStatus: nil, + SystemTime: time.Now().Format(time.RFC3339Nano), + Warnings: []string{}, + }, + BuildahVersion: hostInfo["BuildahVersion"].(string), + CPURealtimePeriod: sysInfo.CPURealtimePeriod, + CPURealtimeRuntime: sysInfo.CPURealtimeRuntime, + CgroupVersion: hostInfo["CgroupVersion"].(string), + Rootless: rootless.IsRootless(), + SwapFree: hostInfo["SwapFree"].(int64), + SwapTotal: hostInfo["SwapTotal"].(int64), + Uptime: hostInfo["uptime"].(string), + } + utils.WriteResponse(w, http.StatusOK, info) +} + +func getGraphStatus(storeInfo map[string]interface{}) [][2]string { + var graphStatus [][2]string + for k, v := range storeInfo["GraphStatus"].(map[string]string) { + graphStatus = append(graphStatus, [2]string{k, v}) + } + return graphStatus +} + +func getSecOpts(sysInfo *sysinfo.SysInfo) []string { + var secOpts []string + if sysInfo.AppArmor { + secOpts = append(secOpts, "name=apparmor") + } + if sysInfo.Seccomp { + // FIXME: get profile name... + secOpts = append(secOpts, fmt.Sprintf("name=seccomp,profile=%s", "default")) + } + return secOpts +} + +func getRuntimes(configInfo *config.Config) map[string]docker.Runtime { + var runtimes = map[string]docker.Runtime{} + for name, paths := range configInfo.OCIRuntimes { + runtimes[name] = docker.Runtime{ + Path: paths[0], + Args: nil, + } + } + return runtimes +} + +func getFdCount() (count int) { + count = -1 + if entries, err := ioutil.ReadDir("/proc/self/fd"); err == nil { + count = len(entries) + } + return +} + +// Just ignoring Container errors here... +func getContainersState(r *libpod.Runtime) map[define.ContainerStatus]int { + var states = map[define.ContainerStatus]int{} + ctnrs, err := r.GetAllContainers() + if err == nil { + for _, ctnr := range ctnrs { + state, err := ctnr.State() + if err != nil { + continue + } + states[state] += 1 + } + } + return states +} + +func getEnv(value string) string { + if v, exists := os.LookupEnv(strings.ToUpper(value)); exists { + return v + } + if v, exists := os.LookupEnv(strings.ToLower(value)); exists { + return v + } + return "" +} diff --git a/pkg/api/handlers/generic/ping.go b/pkg/api/handlers/generic/ping.go new file mode 100644 index 000000000..44a67d53f --- /dev/null +++ b/pkg/api/handlers/generic/ping.go @@ -0,0 +1,25 @@ +package generic + +import ( + "fmt" + "net/http" +) + +func PingGET(w http.ResponseWriter, _ *http.Request) { + setHeaders(w) + fmt.Fprintln(w, "OK") +} + +func PingHEAD(w http.ResponseWriter, _ *http.Request) { + setHeaders(w) + fmt.Fprintln(w, "") +} + +func setHeaders(w http.ResponseWriter) { + w.Header().Set("API-Version", 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") + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/api/handlers/generic/system.go b/pkg/api/handlers/generic/system.go new file mode 100644 index 000000000..edf1f8522 --- /dev/null +++ b/pkg/api/handlers/generic/system.go @@ -0,0 +1,18 @@ +package generic + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + docker "github.com/docker/docker/api/types" +) + +func GetDiskUsage(w http.ResponseWriter, r *http.Request) { + utils.WriteResponse(w, http.StatusOK, handlers.DiskUsage{DiskUsage: docker.DiskUsage{ + LayersSize: 0, + Images: nil, + Containers: nil, + Volumes: nil, + }}) +} diff --git a/pkg/api/handlers/generic/version.go b/pkg/api/handlers/generic/version.go new file mode 100644 index 000000000..39423914d --- /dev/null +++ b/pkg/api/handlers/generic/version.go @@ -0,0 +1,74 @@ +package generic + +import ( + "fmt" + "net/http" + goRuntime "runtime" + "time" + + "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 + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + versionInfo, err := define.GetVersion() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + infoData, err := runtime.Info() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to obtain system memory info")) + return + } + hostInfo := infoData[0].Data + + components := []docker.ComponentVersion{{ + Name: "Podman Engine", + Version: versionInfo.Version, + Details: map[string]string{ + "APIVersion": 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, + "Os": goRuntime.GOOS, + }, + }} + + utils.WriteResponse(w, http.StatusOK, handlers.Version{Version: docker.Version{ + Platform: struct { + Name string + }{ + Name: fmt.Sprintf("%s/%s/%s", goRuntime.GOOS, goRuntime.GOARCH, hostInfo["Distribution"].(map[string]interface{})["distribution"].(string)), + }, + APIVersion: components[0].Details["APIVersion"], + Arch: components[0].Details["Arch"], + BuildTime: components[0].Details["BuildTime"], + Components: components, + Experimental: true, + GitCommit: components[0].Details["GitCommit"], + GoVersion: components[0].Details["GoVersion"], + KernelVersion: components[0].Details["KernelVersion"], + MinAPIVersion: components[0].Details["MinAPIVersion"], + Os: components[0].Details["Os"], + Version: components[0].Version, + }}) +} diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go new file mode 100644 index 000000000..2efeb1379 --- /dev/null +++ b/pkg/api/handlers/handler.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +// Convenience routines to reduce boiler plate in handlers + +func getVar(r *http.Request, k string) string { + return mux.Vars(r)[k] +} + +func hasVar(r *http.Request, k string) bool { + _, found := mux.Vars(r)[k] + return found +} +func getName(r *http.Request) string { + return getVar(r, "name") +} + +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 new file mode 100644 index 000000000..d4cddbfb2 --- /dev/null +++ b/pkg/api/handlers/images.go @@ -0,0 +1,185 @@ +package handlers + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strconv" + + "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 := mux.Vars(r)["name"] + 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 := mux.Vars(r)["name"] + 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) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + name := mux.Vars(r)["name"] + 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 LoadImage(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + //quiet bool # quiet is currently unused + }{ + // 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 + } + + var ( + err error + writer io.Writer + ) + f, err := ioutil.TempFile("", "api_load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) + return + } + if err := SaveFromBody(f, r); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) + return + } + id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) + return + } + utils.WriteResponse(w, http.StatusOK, struct { + Stream string `json:"stream"` + }{ + Stream: fmt.Sprintf("Loaded image: %s\n", id), + }) +} + +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 + } + // TODO filters are a bit undefined here in terms of what exactly the input looks + // like. We need to understand that a bit more. + options := image.SearchOptions{ + Filter: image.SearchFilter{}, + Limit: query.Limit, + } + results, err := image.SearchImages(query.Term, options) + if err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, results) +} diff --git a/pkg/api/handlers/images_build.go b/pkg/api/handlers/images_build.go new file mode 100644 index 000000000..c7c746392 --- /dev/null +++ b/pkg/api/handlers/images_build.go @@ -0,0 +1,233 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/containers/buildah" + "github.com/containers/buildah/imagebuildah" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/storage/pkg/archive" + log "github.com/sirupsen/logrus" +) + +func BuildImage(w http.ResponseWriter, r *http.Request) { + authConfigs := map[string]AuthConfig{} + if hasHeader(r, "X-Registry-Config") { + registryHeader := getHeader(r, "X-Registry-Config") + authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(registryHeader)) + if json.NewDecoder(authConfigsJSON).Decode(&authConfigs) != nil { + utils.BadRequest(w, "X-Registry-Config", registryHeader, json.NewDecoder(authConfigsJSON).Decode(&authConfigs)) + return + } + } + + anchorDir, err := extractTarFile(r, w) + if err != nil { + utils.InternalServerError(w, err) + return + } + // defer os.RemoveAll(anchorDir) + + query := struct { + Dockerfile string `json:"dockerfile"` + Tag string `json:"t"` + ExtraHosts string `json:"extrahosts"` + Remote string `json:"remote"` + Quiet bool `json:"q"` + NoCache bool `json:"nocache"` + CacheFrom string `json:"cachefrom"` + Pull string `json:"pull"` + Rm bool `json:"rm"` + ForceRm bool `json:"forcerm"` + Memory int `json:"memory"` + MemSwap int `json:"memswap"` + CpuShares int `json:"cpushares"` + CpuSetCpus string `json:"cpusetcpus"` + CpuPeriod int `json:"cpuperiod"` + CpuQuota int `json:"cpuquota"` + BuildArgs string `json:"buildargs"` + ShmSize int `json:"shmsize"` + Squash bool `json:"squash"` + Labels string `json:"labels"` + NetworkMode string `json:"networkmode"` + Platform string `json:"platform"` + Target string `json:"target"` + Outputs string `json:"outputs"` + }{ + Dockerfile: "Dockerfile", + Tag: "", + ExtraHosts: "", + Remote: "", + Quiet: false, + NoCache: false, + CacheFrom: "", + Pull: "", + Rm: true, + ForceRm: false, + Memory: 0, + MemSwap: 0, + CpuShares: 0, + CpuSetCpus: "", + CpuPeriod: 0, + CpuQuota: 0, + BuildArgs: "", + ShmSize: 64 * 1024 * 1024, + Squash: false, + Labels: "", + NetworkMode: "", + Platform: "", + Target: "", + Outputs: "", + } + + if err := decodeQuery(r, &query); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) + return + } + + // Tag is the name with optional tag... + var name = query.Tag + var tag string + if strings.Contains(query.Tag, ":") { + tokens := strings.SplitN(query.Tag, ":", 2) + name = tokens[0] + tag = tokens[1] + } + + var buildArgs = map[string]string{} + if found := hasVar(r, "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 hasVar(r, "labels") { + var m = map[string]string{} + if err := json.Unmarshal([]byte(query.Labels), &m); err != nil { + utils.BadRequest(w, "labels", query.Labels, err) + return + } + + for k, v := range m { + labels = append(labels, fmt.Sprintf("%s=%v", k, v)) + } + } + + buildOptions := imagebuildah.BuildOptions{ + ContextDirectory: filepath.Join(anchorDir, "build"), + PullPolicy: 0, + Registry: "", + IgnoreUnrecognizedInstructions: false, + Quiet: query.Quiet, + Isolation: 0, + Runtime: "", + RuntimeArgs: nil, + TransientMounts: nil, + Compression: 0, + Args: buildArgs, + Output: name, + AdditionalTags: []string{tag}, + Log: nil, + In: nil, + Out: nil, + Err: nil, + SignaturePolicyPath: "", + ReportWriter: nil, + OutputFormat: "", + SystemContext: nil, + NamespaceOptions: nil, + ConfigureNetwork: 0, + CNIPluginPath: "", + CNIConfigDir: "", + IDMappingOptions: nil, + AddCapabilities: nil, + DropCapabilities: nil, + CommonBuildOpts: &buildah.CommonBuildOptions{}, + DefaultMountsFilePath: "", + IIDFile: "", + Squash: query.Squash, + Labels: labels, + Annotations: nil, + OnBuild: nil, + Layers: false, + NoCache: query.NoCache, + RemoveIntermediateCtrs: query.Rm, + ForceRmIntermediateCtrs: query.ForceRm, + BlobDirectory: "", + Target: query.Target, + Devices: nil, + } + + id, _, err := getRuntime(r).Build(r.Context(), buildOptions, query.Dockerfile) + if err != nil { + utils.InternalServerError(w, err) + } + + // Find image ID that was built... + utils.WriteResponse(w, http.StatusOK, + struct { + Stream string `json:"stream"` + }{ + Stream: fmt.Sprintf("Successfully built %s\n", id), + }) +} + +func extractTarFile(r *http.Request, w http.ResponseWriter) (string, error) { + var ( + // length int64 + // n int64 + copyErr error + ) + + // build a home for the request body + anchorDir, err := ioutil.TempDir("", "libpod_builder") + if err != nil { + return "", err + } + buildDir := filepath.Join(anchorDir, "build") + + path := filepath.Join(anchorDir, "tarBall") + tarBall, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return "", err + } + defer tarBall.Close() + + // if hasHeader(r, "Content-Length") { + // length, err := strconv.ParseInt(getHeader(r, "Content-Length"), 10, 64) + // if err != nil { + // return "", errors.New(fmt.Sprintf("Failed request: unable to parse Content-Length of '%s'", getHeader(r, "Content-Length"))) + // } + // n, copyErr = io.CopyN(tarBall, r.Body, length+1) + // } else { + _, copyErr = io.Copy(tarBall, r.Body) + // } + r.Body.Close() + + if copyErr != nil { + utils.InternalServerError(w, + fmt.Errorf("failed Request: Unable to copy tar file from request body %s", r.RequestURI)) + } + log.Debugf("Content-Length: %s", getVar(r, "Content-Length")) + + // if hasHeader(r, "Content-Length") && n != length { + // return "", errors.New(fmt.Sprintf("Failed request: Given Content-Length does not match file size %d != %d", n, length)) + // } + + _, _ = tarBall.Seek(0, 0) + if err := archive.Untar(tarBall, buildDir, &archive.TarOptions{}); err != nil { + return "", err + } + return anchorDir, nil +} diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go new file mode 100644 index 000000000..bfb028b1b --- /dev/null +++ b/pkg/api/handlers/libpod/containers.go @@ -0,0 +1,186 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "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) { + handlers.StopContainer(w, r) +} + +func ContainerExists(w http.ResponseWriter, r *http.Request) { + // 404 no such container + // 200 ok + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + _, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func RemoveContainer(w http.ResponseWriter, r *http.Request) { + // 204 no error + // 400 bad param + // 404 no such container + // 409 conflict + // 500 internal error + 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) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Filter []string `schema:"filter"` + Last int `schema:"last"` + Size bool `schema:"size"` + Sync bool `schema:"sync"` + }{ + // 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 + } + runtime := r.Context().Value("runtime").(*libpod.Runtime) + opts := shared.PsOptions{ + All: true, + Last: query.Last, + Size: query.Size, + Sort: "", + Namespace: true, + Sync: query.Sync, + } + + pss, err := shared.GetPsContainerOutput(runtime, opts, query.Filter, 2) + if err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, pss) +} + +func GetContainer(w http.ResponseWriter, r *http.Request) { + 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, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + container, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + data, err := container.Inspect(query.Size) + if err != nil { + utils.InternalServerError(w, err) + return + } + 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) { + _, err := utils.WaitContainer(w, r) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func PruneContainers(w http.ResponseWriter, r *http.Request) { + // TODO Needs rebase to get filers; Also would be handy to define + // an actual libpod container prune method. + // force + // filters +} + +func LogsFromContainer(w http.ResponseWriter, r *http.Request) { + // follow + // since + // timestamps + // tail string +} +func StatsContainer(w http.ResponseWriter, r *http.Request) { + //stream +} +func CreateContainer(w http.ResponseWriter, r *http.Request) { + +} + +func MountContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + conn, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + m, err := conn.Mount() + if err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, m) +} + +func ShowMountedContainers(w http.ResponseWriter, r *http.Request) { + response := make(map[string]string) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + conns, err := runtime.GetAllContainers() + if err != nil { + utils.InternalServerError(w, err) + } + for _, conn := range conns { + mounted, mountPoint, err := conn.Mounted() + if err != nil { + utils.InternalServerError(w, err) + } + if !mounted { + continue + } + response[conn.ID()] = mountPoint + } + utils.WriteResponse(w, http.StatusOK, response) +} diff --git a/pkg/api/handlers/libpod/healthcheck.go b/pkg/api/handlers/libpod/healthcheck.go new file mode 100644 index 000000000..0d7bf3ea7 --- /dev/null +++ b/pkg/api/handlers/libpod/healthcheck.go @@ -0,0 +1,25 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" +) + +func RunHealthCheck(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 404 no such + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + status, err := runtime.HealthCheck(name) + if err != nil { + if status == libpod.HealthCheckContainerNotFound { + utils.ContainerNotFound(w, name, err) + } + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, status) +} diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go new file mode 100644 index 000000000..0d4e220a8 --- /dev/null +++ b/pkg/api/handlers/libpod/images.go @@ -0,0 +1,165 @@ +package libpod + +import ( + "io/ioutil" + "net/http" + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +// Commit +// author string +// "container" +// repo string +// tag string +// message +// pause bool +// changes []string + +// create + +func ImageExists(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 404 no such + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + + _, 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 + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func ImageTree(w http.ResponseWriter, r *http.Request) { + // tree is a bit of a mess ... logic is in adapter and therefore not callable from here. needs rework + + //name := mux.Vars(r)["name"] + //_, layerInfoMap, _, err := s.Runtime.Tree(name) + //if err != nil { + // Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to find image information for %q", name)) + // return + //} + // it is not clear to me how to deal with this given all the processing of the image + // is in main. we need to discuss how that really should be and return something useful. + handlers.UnsupportedHandler(w, r) +} + +func GetImage(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + newImage, err := handlers.GetImage(r, name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + inspect, err := newImage.Inspect(r.Context()) + if err != nil { + utils.Error(w, "Server error", http.StatusInternalServerError, errors.Wrapf(err, "failed in inspect image %s", inspect.ID)) + return + } + utils.WriteResponse(w, http.StatusOK, inspect) + +} +func GetImages(w http.ResponseWriter, r *http.Request) { + images, err := utils.GetImages(w, r) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) + return + } + var summaries = make([]*handlers.ImageSummary, len(images)) + for j, img := range images { + is, err := handlers.ImageToImageSummary(img) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed transform image summaries")) + return + } + // libpod has additional fields that we need to populate. + is.CreatedTime = img.Created() + is.ReadOnly = img.IsReadOnly() + summaries[j] = is + } + utils.WriteResponse(w, http.StatusOK, summaries) +} + +func PruneImages(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + All bool `schema:"all"` + Filters []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 + } + cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, query.Filters) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, cids) +} + +func ExportImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Compress bool `schema:"compress"` + Format string `schema:"format"` + }{ + // 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 + } + + if len(query.Format) < 1 { + utils.InternalServerError(w, errors.New("format parameter cannot be empty.")) + return + } + + tmpfile, err := ioutil.TempFile("", "api.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + if err := tmpfile.Close(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + return + } + name := mux.Vars(r)["name"] + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, err) + return + } + if err := newImage.Save(r.Context(), name, query.Format, tmpfile.Name(), []string{}, false, query.Compress); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) + return + } + rdr, err := os.Open(tmpfile.Name()) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) + return + } + defer rdr.Close() + defer os.Remove(tmpfile.Name()) + utils.WriteResponse(w, http.StatusOK, rdr) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go new file mode 100644 index 000000000..daaf9d018 --- /dev/null +++ b/pkg/api/handlers/libpod/pods.go @@ -0,0 +1,440 @@ +package libpod + +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/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func PodCreate(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + 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()")) + 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 { + if err := parse.ReadKVStrings(labels, []string{}, input.Labels); 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...) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusCreated, handlers.IDResponse{ID: pod.CgroupParent()}) +} + +func Pods(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + podInspectData []*libpod.PodInspect + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + filters []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 + } + if len(query.filters) > 0 { + utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) + return + } + pods, err := runtime.GetAllPods() + 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) +} + +func PodInspect(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + podData, err := pod.Inspect() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, podData) +} + +func PodStop(w http.ResponseWriter, r *http.Request) { + // 200 + // 304 not modified + // 404 no such + // 500 internal + var ( + stopError error + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + 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 + } + allContainersStopped := true + name := mux.Vars(r)["name"] + 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() + 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 { + alreadyStopped := errors.Errorf("pod %s is already stopped", pod.ID()) + utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyStopped) + return + } + + if query.timeout > 0 { + _, stopError = pod.StopWithTimeout(r.Context(), false, query.timeout) + } else { + _, stopError = pod.Stop(r.Context(), false) + } + if stopError != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func PodStart(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + allContainersRunning := true + name := mux.Vars(r)["name"] + 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() + 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 { + alreadyRunning := errors.Errorf("pod %s is already running", pod.ID()) + utils.Error(w, "Something went wrong", http.StatusNotModified, alreadyRunning) + return + } + if _, err := pod.Start(r.Context()); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func PodDelete(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + force bool `schema:"force"` + }{ + // 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 := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + if err := runtime.RemovePod(r.Context(), pod, true, query.force); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func PodRestart(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + _, err = pod.Restart(r.Context()) + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func PodPrune(w http.ResponseWriter, r *http.Request) { + var ( + err error + pods []*libpod.Pod + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + force bool `schema:"force"` + }{ + // 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 + } + + if query.force { + pods, err = runtime.GetAllPods() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + } else { + // TODO We need to make a libpod.PruneVolumes or this code will be a mess. Volumes + // already does this right. It will also help clean this code path up with less + // conditionals. We do this when we integrate with libpod again. + utils.Error(w, "not implemented", http.StatusInternalServerError, errors.New("not implemented")) + return + } + for _, p := range pods { + if err := runtime.RemovePod(r.Context(), p, true, query.force); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func PodPause(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + _, err = pod.Pause() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} + +func PodUnpause(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 404 no such + // 500 internal + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + _, err = pod.Unpause() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func PodKill(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + signal int `schema:"signal"` + }{ + // 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 := mux.Vars(r)["name"] + pod, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + podStates, err := pod.Status() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + hasRunning := false + for _, s := range podStates { + if s == define.ContainerStateRunning { + hasRunning = true + break + } + } + if !hasRunning { + msg := fmt.Sprintf("Container %s is not running", pod.ID()) + utils.Error(w, msg, http.StatusConflict, errors.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) + return + } + // TODO How do we differentiate if a signal was sent vs accepting the pod/container default? + _, err = pod.Kill(uint(query.signal)) + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} + +func PodExists(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := mux.Vars(r)["name"] + _, err := runtime.LookupPod(name) + if err != nil { + utils.PodNotFound(w, name, err) + return + } + utils.WriteResponse(w, http.StatusOK, "") +} diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go new file mode 100644 index 000000000..3e0e597c6 --- /dev/null +++ b/pkg/api/handlers/libpod/volumes.go @@ -0,0 +1,144 @@ +package libpod + +import ( + "encoding/json" + "net/http" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func CreateVolume(w http.ResponseWriter, r *http.Request) { + // 200 ok + // 500 internal + var ( + volumeOptions []libpod.VolumeCreateOption + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + }{ + // override any golang type defaults + } + input := handlers.VolumeCreateConfig{} + 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()")) + return + } + + if len(input.Name) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeName(input.Name)) + } + if len(input.Driver) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeDriver(input.Driver)) + } + if len(input.Label) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) + } + if len(input.Opts) > 0 { + parsedOptions, err := shared.ParseVolumeOptions(input.Opts) + if err != nil { + utils.InternalServerError(w, err) + } + volumeOptions = append(volumeOptions, parsedOptions...) + } + vol, err := runtime.NewVolume(r.Context(), volumeOptions...) + if err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, vol.Name()) +} + +func InspectVolume(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + name := mux.Vars(r)["name"] + vol, err := runtime.GetVolume(name) + if err != nil { + utils.VolumeNotFound(w, name, err) + } + inspect, err := vol.Inspect() + if err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusOK, inspect) +} + +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. + */ + +} + +func PruneVolumes(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + 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]) + } + utils.WriteResponse(w, http.StatusOK, pruned) +} + +func RemoveVolume(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + Force bool `schema:"force"` + }{ + // 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 := mux.Vars(r)["name"] + vol, err := runtime.LookupVolume(name) + if err != nil { + utils.VolumeNotFound(w, name, err) + } + if err := runtime.RemoveVolume(r.Context(), vol, query.Force); err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go new file mode 100644 index 000000000..b677a5a0b --- /dev/null +++ b/pkg/api/handlers/swagger.go @@ -0,0 +1,126 @@ +package handlers + +import ( + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/inspect" + "github.com/docker/docker/api/types" +) + +// History response +// swagger:response DocsHistory +type swagHistory struct { + // in:body + Body struct { + HistoryResponse + } +} + +// Inspect response +// swagger:response DocsImageInspect +type swagImageInspect struct { + // in:body + Body struct { + ImageInspect + } +} + +// Delete response +// swagger:response DocsImageDeleteResponse +type swagImageDeleteResponse struct { + // in:body + Body struct { + image.ImageDeleteResponse + } +} + +// Search results +// swagger:response DocsSearchResponse +type swagSearchResponse struct { + // in:body + Body struct { + image.SearchResult + } +} + +// Inspect image +// swagger:response DocsLibpodInspectImageResponse +type swagLibpodInspectImageResponse struct { + // in:body + Body struct { + inspect.ImageData + } +} + +// Prune containers +// swagger:response DocsContainerPruneReport +type swagContainerPruneReport struct { + // in: body + Body struct { + ContainersPruneReport + } +} + +// Inspect container +// swagger:response DocsContainerInspectResponse +type swagContainerInspectResponse struct { + // in:body + Body struct { + types.ContainerJSON + } +} + +// List processes in container +// swagger:response DockerTopResponse +type swagDockerTopResponse struct { + // in:body + Body struct { + ContainerTopOKBody + } +} + +// List containers +// swagger:response LibpodListContainersResponse +type swagLibpodListContainersResponse struct { + // in:body + Body struct { + shared.PsContainerOutput + } +} + +// Inspect container +// swagger:response LibpodInspectContainerResponse +type swagLibpodInspectContainerResponse struct { + // in:body + Body struct { + libpod.InspectContainerData + } +} + +// List pods +// swagger:response ListPodsResponse +type swagListPodsResponse struct { + // in:body + Body struct { + libpod.PodInspect + } +} + +// Inspect pod +// swagger:response InspectPodResponse +type swagInspectPodResponse struct { + // in:body + Body struct { + libpod.PodInspect + } +} + +// Inspect volume +// swagger:response InspectVolumeResponse +type swagInspectVolumeResponse struct { + // in:body + Body struct { + libpod.InspectVolumeData + } +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go new file mode 100644 index 000000000..9edbbdccc --- /dev/null +++ b/pkg/api/handlers/types.go @@ -0,0 +1,534 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/libpod/events" + libpodImage "github.com/containers/libpod/libpod/image" + docker "github.com/docker/docker/api/types" + dockerContainer "github.com/docker/docker/api/types/container" + dockerEvents "github.com/docker/docker/api/types/events" + dockerNetwork "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" +) + +type AuthConfig struct { + docker.AuthConfig +} + +type ImageInspect struct { + docker.ImageInspect +} + +type ContainerConfig struct { + dockerContainer.Config +} + +type ImageSummary struct { + docker.ImageSummary + CreatedTime time.Time `json:"CreatedTime,omitempty"` + ReadOnly bool `json:"ReadOnly,omitempty"` +} + +type ContainersPruneReport struct { + docker.ContainersPruneReport +} + +type Info struct { + docker.Info + BuildahVersion string + CPURealtimePeriod bool + CPURealtimeRuntime bool + CgroupVersion string + Rootless bool + SwapFree int64 + SwapTotal int64 + Uptime string +} + +type Container struct { + docker.Container + docker.ContainerCreateConfig +} + +type ContainerStats struct { + docker.ContainerStats +} + +type Ping struct { + docker.Ping +} + +type Version struct { + docker.Version +} + +type DiskUsage struct { + docker.DiskUsage +} + +type VolumesPruneReport struct { + docker.VolumesPruneReport +} + +type ImagesPruneReport struct { + docker.ImagesPruneReport +} + +type BuildCachePruneReport struct { + docker.BuildCachePruneReport +} + +type NetworkPruneReport struct { + docker.NetworksPruneReport +} + +type ConfigCreateResponse struct { + docker.ConfigCreateResponse +} + +type PushResult struct { + docker.PushResult +} + +type BuildResult struct { + docker.BuildResult +} + +type ContainerWaitOKBody struct { + StatusCode int + Error struct { + Message string + } +} + +type CreateContainerConfig struct { + Name string + dockerContainer.Config + HostConfig dockerContainer.HostConfig + 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"` +} + +type IDResponse struct { + ID string `json:"id"` +} + +type Stats struct { + docker.StatsJSON +} + +type ContainerTopOKBody struct { + dockerContainer.ContainerTopOKBody + ID string `json:"Id"` +} + +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"` +} + +type Event struct { + dockerEvents.Message +} + +type HistoryResponse struct { + ID string `json:"Id"` + Created int64 `json:"Created"` + CreatedBy string `json:"CreatedBy"` + Tags []string `json:"Tags"` + Size int64 `json:"Size"` + Comment string `json:"Comment"` +} + +type ImageLayer struct{} + +type ImageTreeResponse struct { + ID string `json:"id"` + Tags []string `json:"tags"` + Size string `json:"size"` + Layers []ImageLayer `json:"layers"` +} + +func EventToApiEvent(e *events.Event) *Event { + return &Event{dockerEvents.Message{ + Type: e.Type.String(), + Action: e.Status.String(), + Actor: dockerEvents.Actor{ + ID: e.ID, + Attributes: map[string]string{ + "image": e.Image, + "name": e.Name, + "containerExitCode": strconv.Itoa(e.ContainerExitCode), + }, + }, + Scope: "local", + Time: e.Time.Unix(), + TimeNano: e.Time.UnixNano(), + }} +} + +func ImageToImageSummary(l *libpodImage.Image) (*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 { + // return nil, errors.Wrapf(err, "Failed to obtain ParentID for image %s", l.ID()) + // } + + labels, err := l.Labels(context.TODO()) + if err != nil { + return nil, errors.Wrapf(err, "Failed to obtain Labels for image %s", l.ID()) + } + + size, err := l.Size(context.TODO()) + 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, + } + return &is, nil +} + +func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageInspect, error) { + info, err := l.Inspect(context.Background()) + if err != nil { + return nil, err + } + ports, err := portsToPortSet(info.Config.ExposedPorts) + if err != nil { + return nil, err + } + // TODO the rest of these still need wiring! + config := dockerContainer.Config{ + // Hostname: "", + // Domainname: "", + User: info.User, + // AttachStdin: false, + // AttachStdout: false, + // AttachStderr: false, + ExposedPorts: ports, + // Tty: false, + // OpenStdin: false, + // StdinOnce: false, + Env: info.Config.Env, + Cmd: info.Config.Cmd, + // Healthcheck: nil, + // ArgsEscaped: false, + // Image: "", + // Volumes: nil, + // WorkingDir: "", + // Entrypoint: nil, + // NetworkDisabled: false, + // MacAddress: "", + // OnBuild: nil, + // Labels: nil, + // StopSignal: "", + // StopTimeout: nil, + // Shell: nil, + } + ic, err := l.ToImageRef(ctx) + if err != nil { + return nil, err + } + dockerImageInspect := docker.ImageInspect{ + Architecture: l.Architecture, + Author: l.Author, + Comment: info.Comment, + Config: &config, + Created: l.Created().Format(time.RFC3339Nano), + DockerVersion: "", + GraphDriver: docker.GraphDriverData{}, + ID: fmt.Sprintf("sha256:%s", l.ID()), + Metadata: docker.ImageMetadata{}, + Os: l.Os, + OsVersion: l.Version, + Parent: l.Parent, + RepoDigests: info.RepoDigests, + RepoTags: info.RepoTags, + RootFS: docker.RootFS{}, + Size: info.Size, + Variant: "", + VirtualSize: info.VirtualSize, + } + bi := ic.ConfigInfo() + // For docker images, we need to get the Container id and config + // and populate the image with it. + if bi.MediaType == manifest.DockerV2Schema2ConfigMediaType { + d := manifest.Schema2Image{} + b, err := ic.ConfigBlob(ctx) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &d); err != nil { + return nil, err + } + // populate the Container id into the image + dockerImageInspect.Container = d.Container + containerConfig := dockerContainer.Config{} + configBytes, err := json.Marshal(d.ContainerConfig) + if err != nil { + return nil, err + } + if err := json.Unmarshal(configBytes, &containerConfig); err != nil { + return nil, err + } + // populate the Container config in the image + dockerImageInspect.ContainerConfig = &containerConfig + // populate parent + dockerImageInspect.Parent = d.Parent.String() + } + return &ImageInspect{dockerImageInspect}, nil + +} + +func LibpodToContainer(l *libpod.Container, infoData []define.InfoData) (*Container, error) { + imageId, imageName := l.Image() + sizeRW, err := l.RWSize() + if err != nil { + return nil, err + } + + SizeRootFs, err := l.RootFsSize() + if err != nil { + return nil, err + } + + state, err := l.State() + if err != nil { + return nil, err + } + + return &Container{docker.Container{ + ID: l.ID(), + Names: []string{l.Name()}, + Image: imageName, + ImageID: imageId, + Command: strings.Join(l.Command(), " "), + Created: l.CreatedTime().Unix(), + Ports: nil, + SizeRw: sizeRW, + SizeRootFs: SizeRootFs, + Labels: l.Labels(), + State: string(state), + Status: "", + HostConfig: struct { + NetworkMode string `json:",omitempty"` + }{ + "host"}, + NetworkSettings: nil, + Mounts: nil, + }, + docker.ContainerCreateConfig{}, + }, nil +} + +func LibpodToContainerJSON(l *libpod.Container) (*docker.ContainerJSON, error) { + _, imageName := l.Image() + inspect, err := l.Inspect(true) + if err != nil { + return nil, err + } + i, err := json.Marshal(inspect.State) + if err != nil { + return nil, err + } + state := docker.ContainerState{} + if err := json.Unmarshal(i, &state); err != nil { + return nil, err + } + + // docker considers paused to be running + if state.Paused { + state.Running = true + } + + h, err := json.Marshal(inspect.HostConfig) + if err != nil { + return nil, err + } + hc := dockerContainer.HostConfig{} + if err := json.Unmarshal(h, &hc); err != nil { + return nil, err + } + g, err := json.Marshal(inspect.GraphDriver) + if err != nil { + return nil, err + } + graphDriver := docker.GraphDriverData{} + if err := json.Unmarshal(g, &graphDriver); err != nil { + return nil, err + } + + cb := docker.ContainerJSONBase{ + ID: l.ID(), + Created: l.CreatedTime().String(), + Path: "", + Args: nil, + State: &state, + Image: imageName, + ResolvConfPath: inspect.ResolvConfPath, + HostnamePath: inspect.HostnamePath, + HostsPath: inspect.HostsPath, + LogPath: l.LogPath(), + Node: nil, + Name: l.Name(), + RestartCount: 0, + Driver: inspect.Driver, + Platform: "linux", + MountLabel: inspect.MountLabel, + ProcessLabel: inspect.ProcessLabel, + AppArmorProfile: inspect.AppArmorProfile, + ExecIDs: inspect.ExecIDs, + HostConfig: &hc, + GraphDriver: graphDriver, + SizeRw: inspect.SizeRw, + SizeRootFs: &inspect.SizeRootFs, + } + + stopTimeout := int(l.StopTimeout()) + + ports := make(nat.PortSet) + for p := range inspect.HostConfig.PortBindings { + splitp := strings.Split(p, "/") + port, err := nat.NewPort(splitp[0], splitp[1]) + if err != nil { + return nil, err + } + ports[port] = struct{}{} + } + + config := dockerContainer.Config{ + Hostname: l.Hostname(), + Domainname: inspect.Config.DomainName, + User: l.User(), + AttachStdin: inspect.Config.AttachStdin, + AttachStdout: inspect.Config.AttachStdout, + AttachStderr: inspect.Config.AttachStderr, + ExposedPorts: ports, + Tty: inspect.Config.Tty, + OpenStdin: inspect.Config.OpenStdin, + StdinOnce: inspect.Config.StdinOnce, + Env: inspect.Config.Env, + Cmd: inspect.Config.Cmd, + Healthcheck: nil, + ArgsEscaped: false, + Image: imageName, + Volumes: nil, + WorkingDir: l.WorkingDir(), + Entrypoint: l.Entrypoint(), + NetworkDisabled: false, + MacAddress: "", + OnBuild: nil, + Labels: l.Labels(), + StopSignal: string(l.StopSignal()), + StopTimeout: &stopTimeout, + Shell: nil, + } + + m, err := json.Marshal(inspect.Mounts) + if err != nil { + return nil, err + } + mounts := []docker.MountPoint{} + if err := json.Unmarshal(m, &mounts); err != nil { + return nil, err + } + + networkSettingsDefault := docker.DefaultNetworkSettings{ + EndpointID: "", + Gateway: "", + GlobalIPv6Address: "", + GlobalIPv6PrefixLen: 0, + IPAddress: "", + IPPrefixLen: 0, + IPv6Gateway: "", + MacAddress: l.Config().StaticMAC.String(), + } + + networkSettings := docker.NetworkSettings{ + NetworkSettingsBase: docker.NetworkSettingsBase{}, + DefaultNetworkSettings: networkSettingsDefault, + Networks: nil, + } + + c := docker.ContainerJSON{ + ContainerJSONBase: &cb, + Mounts: mounts, + Config: &config, + NetworkSettings: &networkSettings, + } + return &c, nil +} + +// portsToPortSet converts libpods exposed ports to dockers structs +func portsToPortSet(input map[string]struct{}) (nat.PortSet, error) { + ports := make(nat.PortSet) + for k := range input { + npTCP, err := nat.NewPort("tcp", k) + if err != nil { + return nil, errors.Wrapf(err, "unable to create tcp port from %s", k) + } + npUDP, err := nat.NewPort("udp", k) + if err != nil { + return nil, errors.Wrapf(err, "unable to create udp port from %s", k) + } + ports[npTCP] = struct{}{} + ports[npUDP] = struct{}{} + } + return ports, nil +} diff --git a/pkg/api/handlers/unsupported.go b/pkg/api/handlers/unsupported.go new file mode 100644 index 000000000..956d31f8b --- /dev/null +++ b/pkg/api/handlers/unsupported.go @@ -0,0 +1,17 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/utils" + log "github.com/sirupsen/logrus" +) + +func UnsupportedHandler(w http.ResponseWriter, r *http.Request) { + msg := fmt.Sprintf("Path %s is not supported", r.URL.Path) + log.Infof("Request Failed: %s", msg) + + utils.WriteJSON(w, http.StatusInternalServerError, + utils.ErrorModel{Message: msg}) +} diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go new file mode 100644 index 000000000..64d3d378a --- /dev/null +++ b/pkg/api/handlers/utils/containers.go @@ -0,0 +1,103 @@ +package utils + +import ( + "fmt" + "net/http" + "syscall" + "time" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func KillContainer(w http.ResponseWriter, r *http.Request) (*libpod.Container, error) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decorder").(*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 := mux.Vars(r)["name"] + 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 := mux.Vars(r)["name"] + 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) { + 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"` + }{ + // Override golang default values for types + } + 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 0, err + } + + if len(query.Condition) > 0 { + return 0, errors.Errorf("the condition parameter is not supported") + } + + name := mux.Vars(r)["name"] + 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() +} diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go new file mode 100644 index 000000000..3ec0742bd --- /dev/null +++ b/pkg/api/handlers/utils/errors.go @@ -0,0 +1,88 @@ +package utils + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/libpod/define" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +var ( + ErrLinkNotSupport = errors.New("Link is not supported") +) + +// Error formats an API response to an error +// +// apiMessage and code must match the container API, and are sent to client +// err is logged on the system running the podman service +func Error(w http.ResponseWriter, apiMessage string, code int, err error) { + // Log detailed message of what happened to machine running podman service + log.Infof("Request Failed(%s): %s", http.StatusText(code), err.Error()) + em := ErrorModel{ + Because: (errors.Cause(err)).Error(), + Message: err.Error(), + } + WriteJSON(w, code, em) +} + +func VolumeNotFound(w http.ResponseWriter, nameOrId string, err error) { + if errors.Cause(err) != define.ErrNoSuchVolume { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such volume: %s", nameOrId) + Error(w, msg, http.StatusNotFound, err) +} +func ContainerNotFound(w http.ResponseWriter, nameOrId string, err error) { + if errors.Cause(err) != define.ErrNoSuchCtr { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such container: %s", nameOrId) + Error(w, msg, http.StatusNotFound, err) +} + +func ImageNotFound(w http.ResponseWriter, nameOrId string, err error) { + if errors.Cause(err) != define.ErrNoSuchImage { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such image: %s", nameOrId) + Error(w, msg, http.StatusNotFound, err) +} + +func PodNotFound(w http.ResponseWriter, nameOrId string, err error) { + if errors.Cause(err) != define.ErrNoSuchPod { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such pod: %s", nameOrId) + Error(w, msg, http.StatusNotFound, err) +} + +func ContainerNotRunning(w http.ResponseWriter, containerID string, err error) { + msg := fmt.Sprintf("Container %s is not running", containerID) + Error(w, msg, http.StatusConflict, err) +} + +func InternalServerError(w http.ResponseWriter, err error) { + Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, err) +} + +func BadRequest(w http.ResponseWriter, key string, value string, err error) { + e := errors.Wrapf(err, "Failed to parse query parameter '%s': %q", key, value) + Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, e) +} + +type ErrorModel struct { + // root cause + Because string `json:"cause"` + // error message + Message string `json:"message"` +} + +func (e ErrorModel) Error() string { + return e.Message +} + +func (e ErrorModel) Cause() error { + return errors.New(e.Because) +} diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go new file mode 100644 index 000000000..0815e6eca --- /dev/null +++ b/pkg/api/handlers/utils/handler.go @@ -0,0 +1,44 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + log "github.com/sirupsen/logrus" +) + +// WriteResponse encodes the given value as JSON or string and renders it for http client +func WriteResponse(w http.ResponseWriter, code int, value interface{}) { + switch value.(type) { + case string: + w.Header().Set("Content-Type", "text/plain; charset=us-ascii") + w.WriteHeader(code) + + if _, err := fmt.Fprintln(w, value); err != nil { + log.Errorf("unable to send string response: %q", err) + } + case *os.File: + w.Header().Set("Content-Type", "application/octet; charset=us-ascii") + w.WriteHeader(code) + + if _, err := io.Copy(w, value.(*os.File)); err != nil { + log.Errorf("unable to copy to response: %q", err) + } + default: + WriteJSON(w, code, value) + } +} + +func WriteJSON(w http.ResponseWriter, code int, value interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + if err := coder.Encode(value); err != nil { + log.Errorf("unable to write json: %q", err) + } +} diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go new file mode 100644 index 000000000..9445298ca --- /dev/null +++ b/pkg/api/handlers/utils/images.go @@ -0,0 +1,32 @@ +package utils + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/gorilla/schema" +) + +// GetImages is a common function used to get images for libpod and other compatibility +// mechanisms +func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + query := struct { + //all bool # all is currently unused + filters []string + //digests bool # digests is currently unused + }{ + // This is where you can override the golang default value for one of fields + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + return nil, err + } + filters := query.filters + if len(filters) < 1 { + filters = append(filters, fmt.Sprintf("reference=%s", "")) + } + return runtime.ImageRuntime().GetImagesWithFilters(filters) +} diff --git a/pkg/api/server/handler_api.go b/pkg/api/server/handler_api.go new file mode 100644 index 000000000..4b93998ee --- /dev/null +++ b/pkg/api/server/handler_api.go @@ -0,0 +1,37 @@ +package server + +import ( + "context" + "net/http" + + log "github.com/sirupsen/logrus" +) + +// APIHandler is a wrapper to enhance HandlerFunc's and remove redundant code +func APIHandler(ctx context.Context, h http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Debugf("APIHandler -- Method: %s URL: %s", r.Method, r.URL.String()) + if err := r.ParseForm(); err != nil { + log.Infof("Failed Request: unable to parse form: %q", err) + } + + // TODO: Use ConnContext when ported to go 1.13 + c := context.WithValue(r.Context(), "decoder", ctx.Value("decoder")) + c = context.WithValue(c, "runtime", ctx.Value("runtime")) + c = context.WithValue(c, "shutdownFunc", ctx.Value("shutdownFunc")) + r = r.WithContext(c) + + h(w, r) + + shutdownFunc := r.Context().Value("shutdownFunc").(func() error) + if err := shutdownFunc(); err != nil { + log.Errorf("Failed to shutdown Server in APIHandler(): %s", err.Error()) + } + }) +} + +// VersionedPath prepends the version parsing code +// any handler may override this default when registering URL(s) +func VersionedPath(p string) string { + return "/v{version:[0-9][0-9.]*}" + p +} diff --git a/pkg/api/server/register_auth.go b/pkg/api/server/register_auth.go new file mode 100644 index 000000000..9f312683d --- /dev/null +++ b/pkg/api/server/register_auth.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterAuthHandlers(r *mux.Router) error { + r.Handle(VersionedPath("/auth"), APIHandler(s.Context, handlers.UnsupportedHandler)) + return nil +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go new file mode 100644 index 000000000..af1881624 --- /dev/null +++ b/pkg/api/server/register_containers.go @@ -0,0 +1,729 @@ +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/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { + // swagger:operation POST /containers/create containers createContainer + // + // Create a container + // + // --- + // produces: + // - application/json + // parameters: + // - in: query + // name: name + // type: string + // description: container name + // responses: + // '201': + // description: tbd + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '409': + // "$ref": "#/types/ConflictError" + // '500': + // "$ref": "#/types/InternalError" + r.HandleFunc(VersionedPath("/containers/create"), APIHandler(s.Context, generic.CreateContainer)).Methods(http.MethodPost) + // swagger:operation GET /containers/json containers listContainers + // + // List containers + // + // --- + // produces: + // - application/json + // responses: + // '200': + // schema: + // type: array + // items: + // "$ref": "#/responses/Container" + // '400': + // "$ref": "#/responses/BadParamError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/json"), APIHandler(s.Context, generic.ListContainers)).Methods(http.MethodGet) + // swagger:operation POST /containers/prune containers pruneContainers + // + // Prune unused containers + // + // --- + // parameters: + // - in: query + // name: filters + // type: map[string][]string + // description: something + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/DocsContainerPruneReport" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/prune"), APIHandler(s.Context, generic.PruneContainers)).Methods(http.MethodPost) + // swagger:operation DELETE /containers/{nameOrID} containers removeContainer + // + // Delete container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: force + // type: bool + // description: need something + // - in: query + // name: v + // type: bool + // description: need something + // - in: query + // name: link + // type: bool + // description: not supported + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '409': + // "$ref": "#/responses/ConflictError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}"), APIHandler(s.Context, generic.RemoveContainer)).Methods(http.MethodDelete) + // swagger:operation GET /containers/{nameOrID}/json containers getContainer + // + // Inspect Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/DocsContainerInspectResponse" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/json"), APIHandler(s.Context, generic.GetContainer)).Methods(http.MethodGet) + // swagger:operation POST /containers/{nameOrID}/kill containers killContainer + // + // Kill Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: signal + // type: int + // description: signal to be sent to container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '409': + // "$ref": "#/responses/ConflictError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/kill"), APIHandler(s.Context, generic.KillContainer)).Methods(http.MethodPost) + // swagger:operation GET /containers/{nameOrID}/logs containers LogsFromContainer + // + // Get logs from container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: follow + // type: bool + // description: needs description + // - in: query + // name: stdout + // type: bool + // description: needs description + // - in: query + // name: stderr + // type: bool + // description: needs description + // - in: query + // name: since + // type: string + // description: needs description + // - in: query + // name: until + // type: string + // description: needs description + // - in: query + // name: timestamps + // type: bool + // description: needs description + // - in: query + // name: tail + // type: string + // description: needs description + // produces: + // - application/json + // responses: + // '200': + // description: tbd + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/logs"), APIHandler(s.Context, generic.LogsFromContainer)).Methods(http.MethodGet) + // swagger:operation POST /containers/{nameOrID}/pause containers pauseContainer + // + // Pause Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/pause"), APIHandler(s.Context, handlers.PauseContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name:..*}/rename"), APIHandler(s.Context, handlers.UnsupportedHandler)).Methods(http.MethodPost) + // swagger:operation POST /containers/{nameOrID}/restart containers restartContainer + // + // Restart Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: t + // type: int + // description: timeout before sending kill signal to container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/restart"), APIHandler(s.Context, handlers.RestartContainer)).Methods(http.MethodPost) + // swagger:operation POST /containers/{nameOrID}/start containers startContainer + // + // Start a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: detachKeys + // type: string + // description: needs description + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '304': + // "$ref": "#/responses/ContainerAlreadyStartedError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/start"), APIHandler(s.Context, handlers.StartContainer)).Methods(http.MethodPost) + // swagger:operation GET /containers/{nameOrID}/stats containers statsContainer + // + // Get stats for a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: stream + // type: bool + // description: needs description + // produces: + // - application/json + // responses: + // '204': + // description: tbd + // '304': + // "$ref": "#/responses/ContainerAlreadyStartedError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/stats"), APIHandler(s.Context, generic.StatsContainer)).Methods(http.MethodGet) + // swagger:operation POST /containers/{nameOrID}/stop containers stopContainer + // + // Stop a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: t + // type: int + // description: number of seconds to wait before killing container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '304': + // "$ref": "#/responses/ContainerAlreadyStoppedError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/stop"), APIHandler(s.Context, handlers.StopContainer)).Methods(http.MethodPost) + // swagger:operation GET /containers/{nameOrID}/top containers topContainer + // + // List processes running inside a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: ps_args + // type: string + // description: arguments to pass to ps such as aux + // produces: + // - application/json + // responses: + // '200': + // "ref": "#/responses/DockerTopResponse" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/top"), APIHandler(s.Context, handlers.TopContainer)).Methods(http.MethodGet) + // swagger:operation POST /containers/{nameOrID}/unpause containers unpauseContainer + // + // Unpause Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // schema: + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/unpause"), APIHandler(s.Context, handlers.UnpauseContainer)).Methods(http.MethodPost) + // swagger:operation POST /containers/{nameOrID}/wait containers waitContainer + // + // Wait on a container to exit + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: condition + // type: string + // description: Wait until the container reaches the given condition + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/wait"), APIHandler(s.Context, generic.WaitContainer)).Methods(http.MethodPost) + + /* + libpod endpoints + */ + + r.HandleFunc(VersionedPath("/libpod/containers/create"), APIHandler(s.Context, libpod.CreateContainer)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/json containers listContainers + // + // List containers + // + // --- + // produces: + // - application/json + // responses: + // '200': + // schema: + // type: array + // items: + // "$ref": "#/responses/LibpodListContainersResponse" + // '400': + // "$ref": "#/responses/BadParamError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/json"), APIHandler(s.Context, libpod.ListContainers)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/prune containers pruneContainers + // + // Prune unused containers + // + // --- + // parameters: + // - in: query + // name: force + // type: bool + // description: something + // - in: query + // name: filters + // type: map[string][]string + // description: something + // produces: + // - application/json + // responses: + // '200': + // description: TBD + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/prune"), APIHandler(s.Context, libpod.PruneContainers)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/showmounted containers showMounterContainers + // + // Show mounted containers + // + // --- + // produces: + // - application/json + // responses: + // '200': + // description: mounted containers + // schema: + // type: object + // additionalProperties: + // type: string + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/showmounted"), APIHandler(s.Context, libpod.ShowMountedContainers)).Methods(http.MethodGet) + // swagger:operation DELETE /libpod/containers/json containers removeContainer + // + // Delete container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: force + // type: bool + // description: need something + // - in: query + // name: v + // type: bool + // description: need something + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '409': + // "$ref": "#/responses/ConflictError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}"), APIHandler(s.Context, libpod.RemoveContainer)).Methods(http.MethodDelete) + // swagger:operation GET /libpod/containers/{nameOrID}/json containers getContainer + // + // Inspect Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: size + // type: bool + // description: display filesystem usage + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/LibpodInspectContainerResponse" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/json"), APIHandler(s.Context, libpod.GetContainer)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/{nameOrID}/kill containers killContainer + // + // Kill Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: signal + // type: int + // default: 15 + // description: signal to be sent to container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '409': + // "$ref": "#/responses/ConflictError" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/kill"), APIHandler(s.Context, libpod.KillContainer)).Methods(http.MethodGet) + // swagger:operation GET /libpod/containers/{nameOrID}/mount containers mountContainer + // + // Mount a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // description: mounted container + // schema: + // description: id + // type: string + // example: 3c784de79b791b4ebd3ac55e511f97fedc042328499554937a3f8bfd9c1a2cb8 + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/mount"), APIHandler(s.Context, libpod.MountContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/logs"), APIHandler(s.Context, libpod.LogsFromContainer)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/{nameOrID}/pause containers pauseContainer + // + // Pause Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/pause"), APIHandler(s.Context, handlers.PauseContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/restart containers restartContainer + // + // Restart Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: t + // type: int + // description: timeout before sending kill signal to container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/restart"), APIHandler(s.Context, handlers.RestartContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/start containers startContainer + // + // Start a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: detachKeys + // type: string + // description: needs description + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '304': + // "$ref": "#/responses/ContainerAlreadyStartedError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/start"), APIHandler(s.Context, handlers.StartContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/stats"), APIHandler(s.Context, libpod.StatsContainer)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/top"), APIHandler(s.Context, handlers.TopContainer)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/{nameOrID}/unpause containers unpauseContainer + // + // Unpause Container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/unpause"), APIHandler(s.Context, handlers.UnpauseContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/wait containers waitContainer + // + // Wait on a container to exit + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: condition + // type: string + // description: Wait until the container reaches the given condition + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/wait"), APIHandler(s.Context, libpod.WaitContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/exists containers containerExists + // + // Check if container exists + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '204': + // description: container exists + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/exists"), APIHandler(s.Context, libpod.ContainerExists)).Methods(http.MethodGet) + // swagger:operation POST /libpod/containers/{nameOrID}/stop containers stopContainer + // + // Stop a container + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: t + // type: int + // description: number of seconds to wait before killing container + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '304': + // "$ref": "#/responses/ContainerAlreadyStoppedError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/stop"), APIHandler(s.Context, handlers.StopContainer)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/register_distribution.go b/pkg/api/server/register_distribution.go new file mode 100644 index 000000000..23820b4a7 --- /dev/null +++ b/pkg/api/server/register_distribution.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterDistributionHandlers(r *mux.Router) error { + r.HandleFunc(VersionedPath("/distribution/{name:..*}/json"), handlers.UnsupportedHandler) + return nil +} diff --git a/pkg/api/server/register_events.go b/pkg/api/server/register_events.go new file mode 100644 index 000000000..d764fdbb4 --- /dev/null +++ b/pkg/api/server/register_events.go @@ -0,0 +1,32 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterEventsHandlers(r *mux.Router) error { + // swagger:operation GET /events system getEvents + // --- + // summary: Returns events filtered on query parameters + // produces: + // - application/json + // parameters: + // - name: since + // in: query + // description: start streaming events from this time + // - name: until + // in: query + // description: stop streaming events later than this + // - name: filters + // in: query + // description: JSON encoded map[string][]string of constraints + // responses: + // "200": + // description: OK + // "500": + // description: Failed + // "$ref": "#/types/errorModel" + r.Handle(VersionedPath("/events"), APIHandler(s.Context, handlers.GetEvents)) + return nil +} diff --git a/pkg/api/server/register_healthcheck.go b/pkg/api/server/register_healthcheck.go new file mode 100644 index 000000000..e4cc145d5 --- /dev/null +++ b/pkg/api/server/register_healthcheck.go @@ -0,0 +1,13 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerHealthCheckHandlers(r *mux.Router) error { + r.Handle(VersionedPath("/libpod/containers/{name:..*}/runhealthcheck"), APIHandler(s.Context, libpod.RunHealthCheck)).Methods(http.MethodGet) + return nil +} diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go new file mode 100644 index 000000000..488427f3c --- /dev/null +++ b/pkg/api/server/register_images.go @@ -0,0 +1,571 @@ +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/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerImagesHandlers(r *mux.Router) error { + // swagger:operation POST /images/create images createImage + // + // Create an image from an image + // + // --- + // produces: + // - application/json + // parameters: + // - in: query + // name: fromImage + // type: string + // description: needs description + // - in: query + // name: tag + // type: string + // description: needs description + // responses: + // '200': + // schema: + // items: + // $ref: "TBD" + // '404': + // description: repo or image does not exist + // schema: + // $ref: "#/responses/generalError" + // '500': + // description: unexpected error + // schema: + // $ref: '#/responses/GenericError' + r.Handle(VersionedPath("/images/create"), APIHandler(s.Context, generic.CreateImageFromImage)).Methods(http.MethodPost).Queries("fromImage", "{fromImage}") + // swagger:operation POST /images/create images createImage + // + // Create an image from Source + // + // --- + // produces: + // - application/json + // parameters: + // - in: query + // name: fromSrc + // type: string + // description: needs description + // - in: query + // name: changes + // type: TBD + // description: needs description + // responses: + // '200': + // schema: + // items: + // $ref: "TBD" + // '404': + // description: repo or image does not exist + // schema: + // $ref: "#/responses/generalError" + // '500': + // description: unexpected error + // schema: + // $ref: '#/responses/GenericError' + r.Handle(VersionedPath("/images/create"), APIHandler(s.Context, generic.CreateImageFromSrc)).Methods(http.MethodPost).Queries("fromSrc", "{fromSrc}") + // swagger:operation GET /images/json images listImages + // + // List Images + // + // --- + // produces: + // - application/json + // responses: + // '200': + // schema: + // type: array + // items: + // schema: + // $ref: "#/responses/DockerImageSummary" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/json"), APIHandler(s.Context, generic.GetImages)).Methods(http.MethodGet) + // swagger:operation POST /images/load images loadImage + // + // Import image + // + // --- + // parameters: + // - in: query + // name: quiet + // type: bool + // description: not supported + // produces: + // - application/json + // responses: + // '200': + // description: TBD + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + // swagger:operation POST /images/prune images pruneImages + // + // Prune unused images + // + // --- + // parameters: + // - in: query + // name: filters + // type: map[string][]string + // description: not supported + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DocsImageDeleteResponse" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/prune"), APIHandler(s.Context, generic.PruneImages)).Methods(http.MethodPost) + // swagger:operation GET /images/search images searchImages + // + // Search images + // + // --- + // parameters: + // - in: query + // name: term + // type: string + // description: term to search + // - in: query + // name: limit + // type: int + // description: maximum number of results + // - in: query + // name: filters + // type: map[string][]string + // description: TBD + // produces: + // - application/json + // responses: + // '200': + // $ref: "#/responses/DocsSearchResponse" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/search"), APIHandler(s.Context, handlers.SearchImages)).Methods(http.MethodGet) + // swagger:operation DELETE /images/{nameOrID} images removeImage + // + // Remove Image + // + // --- + // parameters: + // - in: query + // name: force + // type: bool + // description: remove the image even if used by containers or has other tags + // - in: query + // name: noprune + // type: bool + // description: not supported + // produces: + // - application/json + // responses: + // '200': + // $ref: "#/responses/DocsImageDeleteResponse" + // '404': + // $ref: '#/responses/BadParamError' + // '409': + // $ref: '#/responses/ConflictError' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/{name:..*}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + // swagger:operation GET /images/{nameOrID}/get images exportImage + // + // Export an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // schema: + // $ref: "TBD" + // description: TBD + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/{name:..*}/get"), APIHandler(s.Context, generic.ExportImage)).Methods(http.MethodGet) + // swagger:operation GET /images/{nameOrID}/history images imageHistory + // + // History of an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // $ref: "#/responses/DocsHistory" + // '404': + // $ref: "#/responses/NoSuchImage" + // '500': + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/images/{name:..*}/history"), APIHandler(s.Context, handlers.HistoryImage)).Methods(http.MethodGet) + // swagger:operation GET /images/{nameOrID}/json images inspectImage + // + // Inspect an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // $ref: "#/responses/DocsImageInspect" + // '404': + // $ref: "#/responses/NoSuchImage" + // '500': + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/images/{name:..*}/json"), APIHandler(s.Context, generic.GetImage)) + // swagger:operation POST /images/{nameOrID}/tag images tagImage + // + // Tag an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: repo + // type: string + // description: the repository to tag in + // - in: query + // name: tag + // type: string + // description: the name of the new tag + // produces: + // - application/json + // responses: + // 201: + // description: no error + // 400: + // $ref: '#/responses/BadParamError' + // 404: + // $ref: '#/responses/NoSuchImage' + // 409: + // $ref: '#/responses/ConflictError' + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/images/{name:..*}/tag"), APIHandler(s.Context, handlers.TagImage)).Methods(http.MethodPost) + // swagger:operation POST /commit/ commit commitContainer + // + // Create a new image from a container + // + // --- + // parameters: + // - in: query + // name: container + // type: string + // description: the name or ID of a container + // - 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: bool + // description: pause the container before committing it + // - in: query + // name: changes + // type: string + // description: instructions to apply while committing in Dockerfile format + // produces: + // - application/json + // responses: + // '201': + // description: no error + // '404': + // $ref: '#/responses/NoSuchImage' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/commit"), APIHandler(s.Context, generic.CommitContainer)).Methods(http.MethodPost) + + /* + libpod endpoints + */ + + // swagger:operation POST /libpod/images/{nameOrID}/exists images imageExists + // + // Check if image exists in local store + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // parameters: + // - in: query + // name: fromImage + // type: string + // description: needs description + // - in: query + // name: tag + // type: string + // description: needs description + // responses: + // '204': + // description: image exists + // '404': + // $ref: '#/responses/NoSuchImage' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:..*}/exists"), APIHandler(s.Context, libpod.ImageExists)) + r.Handle(VersionedPath("/libpod/images/{name:..*}/tree"), APIHandler(s.Context, libpod.ImageTree)) + // swagger:operation GET /libpod/images/{nameOrID}/history images imageHistory + // + // History of an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/HistoryResponse" + // '404': + // $ref: '#/responses/NoSuchImage' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/history"), APIHandler(s.Context, handlers.HistoryImage)).Methods(http.MethodGet) + // swagger:operation GET /libpod/images/json images listImages + // + // List Images + // + // --- + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DockerImageSummary" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/json"), APIHandler(s.Context, libpod.GetImages)).Methods(http.MethodGet) + // swagger:operation POST /libpod/images/load images loadImage + // + // Import image + // + // --- + // parameters: + // - in: query + // name: quiet + // type: bool + // description: not supported + // produces: + // - application/json + // responses: + // '200': + // description: TBD + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + // swagger:operation POST /libpod/images/prune images pruneImages + // + // Prune unused images + // + // --- + // parameters: + // - in: query + // name: filters + // type: map[string][]string + // description: image filters + // - in: query + // name: all + // type: bool + // description: prune all images + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DocsImageDeleteResponse" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/prune"), APIHandler(s.Context, libpod.PruneImages)).Methods(http.MethodPost) + // swagger:operation GET /libpod/images/search images searchImages + // + // Search images + // + // --- + // parameters: + // - in: query + // name: term + // type: string + // description: term to search + // - in: query + // name: limit + // type: int + // description: maximum number of results + // - in: query + // name: filters + // type: map[string][]string + // description: TBD + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DocsSearchResponse" + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/search"), APIHandler(s.Context, handlers.SearchImages)).Methods(http.MethodGet) + // swagger:operation DELETE /libpod/images/{nameOrID} images removeImage + // + // Remove Image + // + // --- + // parameters: + // - in: query + // name: force + // type: bool + // description: remove the image even if used by containers or has other tags + // - in: query + // name: noprune + // type: bool + // description: not supported + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DocsIageDeleteResponse" + // '404': + // $ref: '#/responses/NoSuchImage' + // '409': + // $ref: '#/responses/ConflictError' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:..*}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + // swagger:operation GET /libpod/images/{nameOrID}/get images exportImage + // + // Export an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: format + // type: string + // description: format for exported image + // - in: query + // name: compress + // type: bool + // description: use compression on image + // produces: + // - application/json + // responses: + // '200': + // description: TBD + // '404': + // $ref: '#/responses/NoSuchImage' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:..*}/get"), APIHandler(s.Context, libpod.ExportImage)).Methods(http.MethodGet) + // swagger:operation GET /libpod/images/{nameOrID}/json images inspectImage + // + // Inspect an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // '200': + // schema: + // items: + // $ref: "#/responses/DocsLibpodInspectImageResponse" + // '404': + // $ref: '#/responses/NoSuchImage' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:..*}/json"), APIHandler(s.Context, libpod.GetImage)) + // swagger:operation POST /libpod/images/{nameOrID}/tag images tagImage + // + // Tag an image + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: repo + // type: string + // description: the repository to tag in + // - in: query + // name: tag + // type: string + // description: the name of the new tag + // produces: + // - application/json + // responses: + // '201': + // description: no error + // '400': + // $ref: '#/responses/BadParamError' + // '404': + // $ref: '#/responses/NoSuchImage' + // '409': + // $ref: '#/responses/ConflictError' + // '500': + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:..*}/tag"), APIHandler(s.Context, handlers.TagImage)).Methods(http.MethodPost) + + r.Handle(VersionedPath("/build"), APIHandler(s.Context, handlers.BuildImage)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/register_info.go b/pkg/api/server/register_info.go new file mode 100644 index 000000000..797158553 --- /dev/null +++ b/pkg/api/server/register_info.go @@ -0,0 +1,28 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerInfoHandlers(r *mux.Router) error { + // swagger:operation GET /info libpod getInfo + // + // Returns information on the system and libpod configuration + // + // --- + // produces: + // - application/json + // responses: + // '200': + // schema: + // "$ref": "#/types/Info" + // '500': + // description: unexpected error + // schema: + // "$ref": "#/types/ErrorModel" + r.Handle(VersionedPath("/info"), APIHandler(s.Context, generic.GetInfo)).Methods(http.MethodGet) + return nil +} diff --git a/pkg/api/server/register_monitor.go b/pkg/api/server/register_monitor.go new file mode 100644 index 000000000..e6c235419 --- /dev/null +++ b/pkg/api/server/register_monitor.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterMonitorHandlers(r *mux.Router) error { + r.Handle(VersionedPath("/monitor"), APIHandler(s.Context, handlers.UnsupportedHandler)) + return nil +} diff --git a/pkg/api/server/register_ping.go b/pkg/api/server/register_ping.go new file mode 100644 index 000000000..4956f9822 --- /dev/null +++ b/pkg/api/server/register_ping.go @@ -0,0 +1,17 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerPingHandlers(r *mux.Router) error { + r.Handle("/_ping", APIHandler(s.Context, generic.PingGET)).Methods(http.MethodGet) + r.Handle("/_ping", APIHandler(s.Context, generic.PingHEAD)).Methods("HEAD") + + // libpod + r.Handle("/libpod/_ping", APIHandler(s.Context, generic.PingGET)).Methods(http.MethodGet) + return nil +} diff --git a/pkg/api/server/register_plugins.go b/pkg/api/server/register_plugins.go new file mode 100644 index 000000000..7fd6b9c4c --- /dev/null +++ b/pkg/api/server/register_plugins.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/gorilla/mux" +) + +func (s *APIServer) RegisterPluginsHandlers(r *mux.Router) error { + r.Handle(VersionedPath("/plugins"), APIHandler(s.Context, handlers.UnsupportedHandler)) + return nil +} diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go new file mode 100644 index 000000000..c2e10277e --- /dev/null +++ b/pkg/api/server/register_pods.go @@ -0,0 +1,256 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerPodsHandlers(r *mux.Router) error { + // swagger:operation GET /libpod/pods/json pods ListPods + // + // List Pods + // + // --- + // produces: + // - application/json + // parameters: + // - in: query + // name: filters + // descriptions: needs description and plumbing for filters + // responses: + // '200': + // $ref: "#/responses/ListPodsResponse" + // '400': + // $ref: "#/responses/BadParamError" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/json"), APIHandler(s.Context, libpod.Pods)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/pods/create"), APIHandler(s.Context, libpod.PodCreate)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/prune pods PrunePods + // + // Prune unused pods + // + // --- + // parameters: + // - in: query + // name: force + // description: force delete + // type: bool + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '400': + // $ref: "#/responses/BadParamError" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/prune"), APIHandler(s.Context, libpod.PodPrune)).Methods(http.MethodPost) + // swagger:operation DELETE /libpod/pods/{nameOrID} pods removePod + // + // Remove Pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // - in: query + // name: force + // type: bool + // description: force delete + // responses: + // '204': + // description: no error + // '400': + // $ref: "#/responses/BadParamError" + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}"), APIHandler(s.Context, libpod.PodDelete)).Methods(http.MethodDelete) + // swagger:operation GET /libpod/pods/{nameOrID}/json pods inspectPod + // + // Inspect Pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '200': + // $ref: "#/responses/InspectPodResponse" + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/json"), APIHandler(s.Context, libpod.PodInspect)).Methods(http.MethodGet) + // swagger:operation GET /libpod/pods/{nameOrID}/exists pods podExists + // + // Inspect Pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '204': + // description: pod exists + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/exists"), APIHandler(s.Context, libpod.PodExists)).Methods(http.MethodGet) + // swagger:operation POST /libpod/pods/{nameOrID}/kill pods killPod + // + // Kill a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // - in: query + // name: signal + // type: int + // description: signal to be sent to pod + // responses: + // '204': + // description: no error + // '400': + // $ref: "#/responses/BadParamError" + // '404': + // $ref: "#/responses/NoSuchPod" + // '409': + // $ref: "#/responses/ConflictError" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/kill"), APIHandler(s.Context, libpod.PodKill)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/{nameOrID}/pause pods pausePod + // + // Pause a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '204': + // description: no error + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/pause"), APIHandler(s.Context, libpod.PodPause)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/{nameOrID}/restart pods restartPod + // + // Restart a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '204': + // description: no error + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/restart"), APIHandler(s.Context, libpod.PodRestart)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/{nameOrID}/start pods startPod + // + // Start a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '204': + // description: no error + // '304': + // $ref: "#/responses/PodAlreadyStartedError" + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/start"), APIHandler(s.Context, libpod.PodStart)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/{nameOrID}/stop pods stopPod + // + // Stop a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // - in: query + // name: t + // type: int + // description: timeout + // responses: + // '204': + // description: no error + // '304': + // $ref: "#/responses/PodAlreadyStoppedError" + // '400': + // $ref: "#/responses/BadParamError" + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/stop"), APIHandler(s.Context, libpod.PodStop)).Methods(http.MethodPost) + // swagger:operation POST /libpod/pods/{nameOrID}/unpause pods unpausePod + // + // Unpause a pod + // + // --- + // produces: + // - application/json + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the pod + // responses: + // '204': + // description: no error + // '404': + // $ref: "#/responses/NoSuchPod" + // '500': + // $ref: "#responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/{name:..*}/unpause"), APIHandler(s.Context, libpod.PodUnpause)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/register_swarm.go b/pkg/api/server/register_swarm.go new file mode 100644 index 000000000..63d8acfde --- /dev/null +++ b/pkg/api/server/register_swarm.go @@ -0,0 +1,27 @@ +package server + +import ( + "errors" + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +func (s *APIServer) RegisterSwarmHandlers(r *mux.Router) error { + r.PathPrefix("/v{version:[0-9.]+}/configs/").HandlerFunc(noSwarm) + r.PathPrefix("/v{version:[0-9.]+}/nodes/").HandlerFunc(noSwarm) + r.PathPrefix("/v{version:[0-9.]+}/secrets/").HandlerFunc(noSwarm) + 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) + return nil +} + +// noSwarm returns http.StatusServiceUnavailable rather than something like http.StatusInternalServerError, +// this allows the client to decide if they still can talk to us +func noSwarm(w http.ResponseWriter, r *http.Request) { + logrus.Errorf("%s is not a podman supported service", r.URL.String()) + utils.Error(w, "node is not part of a swarm", http.StatusServiceUnavailable, errors.New("Podman does not support service: "+r.URL.String())) +} diff --git a/pkg/api/server/register_system.go b/pkg/api/server/register_system.go new file mode 100644 index 000000000..f0eaeffd2 --- /dev/null +++ b/pkg/api/server/register_system.go @@ -0,0 +1,11 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerSystemHandlers(r *mux.Router) error { + r.Handle(VersionedPath("/system/df"), APIHandler(s.Context, generic.GetDiskUsage)) + return nil +} diff --git a/pkg/api/server/register_version.go b/pkg/api/server/register_version.go new file mode 100644 index 000000000..94216b1b6 --- /dev/null +++ b/pkg/api/server/register_version.go @@ -0,0 +1,12 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerVersionHandlers(r *mux.Router) error { + r.Handle("/version", APIHandler(s.Context, generic.VersionHandler)) + r.Handle(VersionedPath("/version"), APIHandler(s.Context, generic.VersionHandler)) + return nil +} diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go new file mode 100644 index 000000000..8fe5a67e4 --- /dev/null +++ b/pkg/api/server/register_volumes.go @@ -0,0 +1,85 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { + // swagger:operation POST /libpod/volumes/create volumes createVolume + // + // Create a volume + // + // --- + // produces: + // - application/json + // responses: + // '200': + // description: tbd + // '500': + // "$ref": "#/responses/InternalError" + r.Handle("/libpod/volumes/create", APIHandler(s.Context, libpod.CreateVolume)).Methods(http.MethodPost) + r.Handle("/libpod/volumes/json", APIHandler(s.Context, libpod.ListVolumes)).Methods(http.MethodGet) + // swagger:operation POST /volumes/prune volumes pruneVolumes + // + // Prune volumes + // + // --- + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '500': + // "$ref": "#/responses/InternalError" + r.Handle("/libpod/volumes/prune", APIHandler(s.Context, libpod.PruneVolumes)).Methods(http.MethodPost) + // swagger:operation GET /volumes/{nameOrID}/json volumes inspectVolume + // + // Inspect volume + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the volume + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/InspectVolumeResponse" + // '404': + // "$ref": "#/responses/NoSuchVolume" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle("/libpod/volumes/{name:..*}/json", APIHandler(s.Context, libpod.InspectVolume)).Methods(http.MethodGet) + // swagger:operation DELETE /volumes/{nameOrID} volumes removeVolume + // + // Inspect volume + // + // --- + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the volume + // - in: query + // name: force + // type: bool + // description: force removal + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchVolume" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle("/libpod/volumes/{name:..*}", APIHandler(s.Context, libpod.RemoveVolume)).Methods(http.MethodDelete) + return nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go new file mode 100644 index 000000000..717c7a876 --- /dev/null +++ b/pkg/api/server/server.go @@ -0,0 +1,189 @@ +// Package serviceapi Provides a Container compatible interface. +// +// This documentation describes the HTTP LibPod interface +// +// Schemes: http, https +// Host: podman.io +// BasePath: / +// Version: 0.0.1 +// License: Apache-2.0 https://opensource.org/licenses/Apache-2.0 +// Contact: Podman <podman@lists.podman.io> https://podman.io/community/ +// +// Consumes: +// - application/json +// - application/x-tar +// +// Produces: +// - application/json +// - text/plain +// - text/html +// +// tags: +// - name: "Containers" +// description: manage containers +// - name: "Images" +// description: manage images +// - name: "System" +// description: manage system resources +// +// swagger:meta +package server + +import ( + "context" + "net" + "net/http" + "os" + "os/signal" + "strings" + "time" + + "github.com/containers/libpod/libpod" + "github.com/coreos/go-systemd/activation" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type APIServer struct { + http.Server // Where the HTTP work happens + *schema.Decoder // Decoder for Query parameters to structs + context.Context // Context for graceful server shutdown + *libpod.Runtime // Where the real work happens + net.Listener // mux for routing HTTP API calls to libpod routines + context.CancelFunc // Stop APIServer + *time.Timer // Hold timer for sliding window + time.Duration // Duration of client access sliding window +} + +// NewServer will create and configure a new API HTTP server +func NewServer(runtime *libpod.Runtime) (*APIServer, error) { + listeners, err := activation.Listeners() + if err != nil { + return nil, errors.Wrap(err, "Cannot retrieve file descriptors from systemd") + } + if len(listeners) != 1 { + return nil, errors.Errorf("Wrong number of file descriptors from systemd for socket activation (%d != 1)", len(listeners)) + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit) + + router := mux.NewRouter() + + server := APIServer{ + Server: http.Server{ + Handler: router, + ReadHeaderTimeout: 20 * time.Second, + ReadTimeout: 20 * time.Second, + WriteTimeout: 2 * time.Minute, + }, + Decoder: schema.NewDecoder(), + Context: nil, + Runtime: runtime, + Listener: listeners[0], + CancelFunc: nil, + Duration: 300 * time.Second, + } + server.Timer = time.AfterFunc(server.Duration, func() { + if err := server.Shutdown(); err != nil { + log.Errorf("unable to shutdown server: %q", err) + } + }) + + ctx, cancelFn := context.WithCancel(context.Background()) + + // TODO: Use ConnContext when ported to go 1.13 + ctx = context.WithValue(ctx, "decoder", server.Decoder) + ctx = context.WithValue(ctx, "runtime", runtime) + ctx = context.WithValue(ctx, "shutdownFunc", server.Shutdown) + server.Context = ctx + + server.CancelFunc = cancelFn + server.Decoder.IgnoreUnknownKeys(true) + + router.NotFoundHandler = http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // We can track user errors... + log.Infof("Failed Request: (%d:%s) for %s:'%s'", http.StatusNotFound, http.StatusText(http.StatusNotFound), r.Method, r.URL.String()) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + }, + ) + + for _, fn := range []func(*mux.Router) error{ + server.RegisterAuthHandlers, + server.RegisterContainersHandlers, + server.RegisterDistributionHandlers, + server.registerHealthCheckHandlers, + server.registerImagesHandlers, + server.registerInfoHandlers, + server.RegisterMonitorHandlers, + server.registerPingHandlers, + server.RegisterPluginsHandlers, + server.registerPodsHandlers, + server.RegisterSwarmHandlers, + server.registerSystemHandlers, + server.registerVersionHandlers, + server.registerVolumeHandlers, + } { + if err := fn(router); err != nil { + return nil, err + } + } + + if log.IsLevelEnabled(log.DebugLevel) { + router.Walk(func(route *mux.Route, r *mux.Router, ancestors []*mux.Route) error { // nolint + path, err := route.GetPathTemplate() + if err != nil { + path = "" + } + methods, err := route.GetMethods() + if err != nil { + methods = []string{} + } + log.Debugf("Methods: %s Path: %s", strings.Join(methods, ", "), path) + return nil + }) + } + + return &server, nil +} + +// Serve starts responding to HTTP requests +func (s *APIServer) Serve() error { + defer s.CancelFunc() + + err := s.Server.Serve(s.Listener) + if err != nil && err != http.ErrServerClosed { + return errors.Wrap(err, "Failed to start APIServer") + } + + return nil +} + +// Shutdown is a clean shutdown waiting on existing clients +func (s *APIServer) Shutdown() error { + // We're still in the sliding service window + if s.Timer.Stop() { + s.Timer.Reset(s.Duration) + return nil + } + + // We've been idle for the service window, really shutdown + go func() { + err := s.Server.Shutdown(s.Context) + if err != nil && err != context.Canceled { + log.Errorf("Failed to cleanly shutdown APIServer: %s", err.Error()) + } + }() + + // Wait for graceful shutdown vs. just killing connections and dropping data + <-s.Context.Done() + return nil +} + +// Close immediately stops responding to clients and exits +func (s *APIServer) Close() error { + return s.Server.Close() +} diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go new file mode 100644 index 000000000..0eb57ebab --- /dev/null +++ b/pkg/api/server/swagger.go @@ -0,0 +1,133 @@ +package server + +import ( + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/handlers/utils" +) + +// No such image +// swagger:response NoSuchImage +type swagErrNoSuchImage struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// No such container +// swagger:response NoSuchContainer +type swagErrNoSuchContainer struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// No such volume +// swagger:response NoSuchVolume +type swagErrNoSuchVolume struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// No such pod +// swagger:response NoSuchPod +type swagErrNoSuchPod struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Internal error +// swagger:response InternalError +type swagInternalError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Generic error +// swagger:response GenericError +type swagGenericError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Conflict error +// swagger:response ConflictError +type swagConflictError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Bad parameter +// swagger:response BadParamError +type swagBadParamError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Container already started +// swagger:response ContainerAlreadyStartedError +type swagContainerAlreadyStartedError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Container already stopped +// swagger:response ContainerAlreadyStoppedError +type swagContainerAlreadyStopped struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Pod already started +// swagger:response PodAlreadyStartedError +type swagPodAlreadyStartedError struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Pod already stopped +// swagger:response PodAlreadyStoppedError +type swagPodAlreadyStopped struct { + // in:body + Body struct { + utils.ErrorModel + } +} + +// Image summary +// swagger:response DockerImageSummary +type swagImageSummary struct { + // in:body + Body struct { + handlers.ImageSummary + } +} + +// List Containers +// swagger:response DocsListContainer +type swagListContainers struct { + // in:body + Body struct { + // This causes go-swagger to crash + //handlers.Container + } +} diff --git a/pkg/bindings/containers.go b/pkg/bindings/containers.go new file mode 100644 index 000000000..cd0b09767 --- /dev/null +++ b/pkg/bindings/containers.go @@ -0,0 +1,137 @@ +package bindings + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" +) + +func (c Connection) ListContainers(filter []string, last int, size, sync bool) ([]shared.PsContainerOutput, error) { // nolint:typecheck + images := []shared.PsContainerOutput{} + params := make(map[string]string) + params["last"] = strconv.Itoa(last) + params["size"] = strconv.FormatBool(size) + params["sync"] = strconv.FormatBool(sync) + response, err := c.newRequest(http.MethodGet, "/containers/json", nil, params) + if err != nil { + return images, err + } + return images, response.Process(nil) +} + +func (c Connection) PruneContainers() ([]string, error) { + var ( + pruned []string + ) + response, err := c.newRequest(http.MethodPost, "/containers/prune", nil, nil) + if err != nil { + return pruned, err + } + return pruned, response.Process(nil) +} + +func (c Connection) RemoveContainer(nameOrID string, force, volumes bool) error { + params := make(map[string]string) + params["force"] = strconv.FormatBool(force) + params["vols"] = strconv.FormatBool(volumes) + response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/containers/%s", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) InspectContainer(nameOrID string, size bool) (*libpod.InspectContainerData, error) { + params := make(map[string]string) + params["size"] = strconv.FormatBool(size) + response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/json", nameOrID), nil, params) + if err != nil { + return nil, err + } + inspect := libpod.InspectContainerData{} + return &inspect, response.Process(&inspect) +} + +func (c Connection) KillContainer(nameOrID string, signal int) error { + params := make(map[string]string) + params["signal"] = strconv.Itoa(signal) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/kill", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) + +} +func (c Connection) ContainerLogs() {} +func (c Connection) PauseContainer(nameOrID string) error { + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/pause", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) RestartContainer(nameOrID string, timeout int) error { + // TODO how do we distinguish between an actual zero value and not wanting to change the timeout value + params := make(map[string]string) + params["timeout"] = strconv.Itoa(timeout) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/restart", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) StartContainer(nameOrID, detachKeys string) error { + params := make(map[string]string) + if len(detachKeys) > 0 { + params["detachKeys"] = detachKeys + } + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/start", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) ContainerStats() {} +func (c Connection) ContainerTop() {} + +func (c Connection) UnpauseContainer(nameOrID string) error { + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/unpause", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) WaitContainer(nameOrID string) error { + _, err := http.Post(c.makeEndpoint(fmt.Sprintf("containers/%s/wait", nameOrID)), "application/json", nil) + return err +} + +func (c Connection) ContainerExists(nameOrID string) (bool, error) { + response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/containers/%s/exists", nameOrID))) + if err != nil { + return false, err + } + if response.StatusCode == http.StatusOK { + return true, nil + } + return false, nil +} + +func (c Connection) StopContainer(nameOrID string, timeout int) error { + // TODO we might need to distinguish whether a timeout is desired; a zero, the int + // zero value is valid; what do folks want to do? + params := make(map[string]string) + params["t"] = strconv.Itoa(timeout) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/stop", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/generate.go b/pkg/bindings/generate.go new file mode 100644 index 000000000..534909062 --- /dev/null +++ b/pkg/bindings/generate.go @@ -0,0 +1,4 @@ +package bindings + +func (c Connection) GenerateKube() {} +func (c Connection) GenerateSystemd() {} diff --git a/pkg/bindings/healthcheck.go b/pkg/bindings/healthcheck.go new file mode 100644 index 000000000..32515e332 --- /dev/null +++ b/pkg/bindings/healthcheck.go @@ -0,0 +1,19 @@ +package bindings + +import ( + "fmt" + "net/http" + + "github.com/containers/libpod/libpod" +) + +func (c Connection) RunHealthCheck(nameOrID string) (*libpod.HealthCheckStatus, error) { + var ( + status libpod.HealthCheckStatus + ) + response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/runhealthcheck", nameOrID), nil, nil) + if err != nil { + return nil, err + } + return &status, response.Process(&status) +} diff --git a/pkg/bindings/info.go b/pkg/bindings/info.go new file mode 100644 index 000000000..5f318d652 --- /dev/null +++ b/pkg/bindings/info.go @@ -0,0 +1,3 @@ +package bindings + +func (c Connection) Info() {} diff --git a/pkg/bindings/mount.go b/pkg/bindings/mount.go new file mode 100644 index 000000000..2e3d6d7f6 --- /dev/null +++ b/pkg/bindings/mount.go @@ -0,0 +1,26 @@ +package bindings + +import ( + "fmt" + "net/http" +) + +func (c Connection) MountContainer(nameOrID string) (string, error) { + var ( + path string + ) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/mount", nameOrID), nil, nil) + if err != nil { + return path, err + } + return path, response.Process(&path) +} + +func (c Connection) GetMountedContainerPaths() (map[string]string, error) { + mounts := make(map[string]string) + response, err := c.newRequest(http.MethodGet, "/containers/showmounted", nil, nil) + if err != nil { + return mounts, err + } + return mounts, response.Process(&mounts) +} diff --git a/pkg/bindings/network.go b/pkg/bindings/network.go new file mode 100644 index 000000000..383615e5d --- /dev/null +++ b/pkg/bindings/network.go @@ -0,0 +1,37 @@ +package bindings + +import ( + "fmt" + "net/http" + + "github.com/containernetworking/cni/libcni" +) + +func (c Connection) CreateNetwork() {} +func (c Connection) InspectNetwork(nameOrID string) (map[string]interface{}, error) { + n := make(map[string]interface{}) + response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/networks/%s/json", nameOrID), nil, nil) + if err != nil { + return n, err + } + return n, response.Process(&n) +} + +func (c Connection) RemoveNetwork(nameOrID string) error { + response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/networks/%s", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) ListNetworks() ([]*libcni.NetworkConfigList, error) { + var ( + netList []*libcni.NetworkConfigList + ) + response, err := c.newRequest(http.MethodGet, "/networks/json", nil, nil) + if err != nil { + return netList, err + } + return netList, response.Process(&netList) +} diff --git a/pkg/bindings/play.go b/pkg/bindings/play.go new file mode 100644 index 000000000..a9dee82b1 --- /dev/null +++ b/pkg/bindings/play.go @@ -0,0 +1,3 @@ +package bindings + +func (c Connection) PlayKube() {} diff --git a/pkg/bindings/pods.go b/pkg/bindings/pods.go new file mode 100644 index 000000000..eac9d2ef5 --- /dev/null +++ b/pkg/bindings/pods.go @@ -0,0 +1,128 @@ +package bindings + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" +) + +func (c Connection) CreatePod() error { + // TODO + return ErrNotImplemented +} + +func (c Connection) PodExists(nameOrID string) (bool, error) { + response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/pods/%s/exists", nameOrID))) + if err != nil { + return false, err + } + return response.StatusCode == http.StatusOK, err +} + +func (c Connection) InspectPod(nameOrID string) (*libpod.PodInspect, error) { + inspect := libpod.PodInspect{} + response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/pods/%s/json", nameOrID), nil, nil) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +func (c Connection) KillPod(nameOrID string, signal int) error { + params := make(map[string]string) + params["signal"] = strconv.Itoa(signal) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/kill", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) PausePod(nameOrID string) error { + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/pause", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) PrunePods(force bool) error { + params := make(map[string]string) + params["force"] = strconv.FormatBool(force) + response, err := c.newRequest(http.MethodPost, "/pods/prune", nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) ListPods(filters []string) (*[]libpod.PodInspect, error) { + var ( + inspect []libpod.PodInspect + ) + params := make(map[string]string) + // TODO I dont remember how to do this for []string{} + // FIXME + //params["filters"] = strconv.FormatBool(force) + response, err := c.newRequest(http.MethodPost, "/pods/json", nil, params) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +func (c Connection) RestartPod(nameOrID string) error { + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/restart", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) RemovePod(nameOrID string, force bool) error { + params := make(map[string]string) + params["force"] = strconv.FormatBool(force) + response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) StartPod(nameOrID string) error { + response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s/start", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) PodStats() error { + // TODO + return ErrNotImplemented +} + +func (c Connection) StopPod(nameOrID string, timeout int) error { + params := make(map[string]string) + params["t"] = strconv.Itoa(timeout) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/stop", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} + +func (c Connection) PodTop() error { + // TODO + return ErrNotImplemented // nolint:typecheck +} + +func (c Connection) UnpausePod(nameOrID string) error { + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/unpause", nameOrID), nil, nil) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/search.go b/pkg/bindings/search.go new file mode 100644 index 000000000..0f462357c --- /dev/null +++ b/pkg/bindings/search.go @@ -0,0 +1,39 @@ +package bindings + +import ( + "net/http" + "strconv" + + "github.com/containers/libpod/libpod/image" +) + +type ImageSearchFilters struct { + Automated bool `json:"automated"` + Official bool `json:"official"` + Stars int `json:"stars"` +} + +// TODO This method can be concluded when we determine how we want the filters to work on the +// API end +func (i *ImageSearchFilters) ToMapJSON() string { + return "" +} + +func (c Connection) SearchImages(term string, limit int, filters *ImageSearchFilters) ([]image.SearchResult, error) { + var ( + searchResults []image.SearchResult + ) + params := make(map[string]string) + params["term"] = term + if limit > 0 { + params["limit"] = strconv.Itoa(limit) + } + if filters != nil { + params["filters"] = filters.ToMapJSON() + } + response, err := c.newRequest(http.MethodGet, "/images/search", nil, params) + if err != nil { + return searchResults, nil + } + return searchResults, response.Process(&searchResults) +} diff --git a/pkg/bindings/version.go b/pkg/bindings/version.go new file mode 100644 index 000000000..c833a644c --- /dev/null +++ b/pkg/bindings/version.go @@ -0,0 +1,3 @@ +package bindings + +func (c Connection) Version() {} diff --git a/pkg/bindings/volumes.go b/pkg/bindings/volumes.go new file mode 100644 index 000000000..27e6f9efa --- /dev/null +++ b/pkg/bindings/volumes.go @@ -0,0 +1,60 @@ +package bindings + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" +) + +func (c Connection) CreateVolume(config handlers.VolumeCreateConfig) (string, error) { + var ( + volumeID string + ) + response, err := c.newRequest(http.MethodPost, "/volumes/create", nil, nil) + if err != nil { + return volumeID, err + } + return volumeID, response.Process(&volumeID) +} + +func (c Connection) InspectVolume(nameOrID string) (*libpod.InspectVolumeData, error) { + var ( + inspect libpod.InspectVolumeData + ) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/volumes/%s/json", nameOrID), nil, nil) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +func (c Connection) ListVolumes() error { + // TODO + // The API side of things for this one does a lot in main and therefore + // is not implemented yet. + return ErrNotImplemented // nolint:typecheck +} + +func (c Connection) PruneVolumes() ([]string, error) { + var ( + pruned []string + ) + response, err := c.newRequest(http.MethodPost, "/volumes/prune", nil, nil) + if err != nil { + return pruned, err + } + return pruned, response.Process(&pruned) +} + +func (c Connection) RemoveVolume(nameOrID string, force bool) error { + params := make(map[string]string) + params["force"] = strconv.FormatBool(force) + response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/volumes/prune", nameOrID), nil, params) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/network/config.go b/pkg/network/config.go index a41455f68..e5c981419 100644 --- a/pkg/network/config.go +++ b/pkg/network/config.go @@ -2,6 +2,7 @@ package network import ( "encoding/json" + "errors" "net" ) @@ -19,6 +20,10 @@ const ( DefaultPodmanDomainName = "dns.podman" ) +var ( + ErrNetworkNotFound = errors.New("network not found") +) + // GetDefaultPodmanNetwork outputs the default network for podman func GetDefaultPodmanNetwork() (*net.IPNet, error) { _, n, err := net.ParseCIDR("10.88.1.0/24") diff --git a/pkg/network/files.go b/pkg/network/files.go index 2f3932974..92cadcf0c 100644 --- a/pkg/network/files.go +++ b/pkg/network/files.go @@ -2,6 +2,7 @@ package network import ( "encoding/json" + "fmt" "io/ioutil" "sort" "strings" @@ -46,7 +47,7 @@ func GetCNIConfigPathByName(name string) (string, error) { return confFile, nil } } - return "", errors.Errorf("unable to find network configuration for %s", name) + return "", errors.Wrap(ErrNetworkNotFound, fmt.Sprintf("unable to find network configuration for %s", name)) } // ReadRawCNIConfByName reads the raw CNI configuration for a CNI diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 244a8d1cd..6d058229b 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -282,10 +282,13 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l options = append(options, libpod.WithStopSignal(c.StopSignal)) options = append(options, libpod.WithStopTimeout(c.StopTimeout)) - logPath := getLoggingPath(c.LogDriverOpt) + logPath, logTag := getLoggingOpts(c.LogDriverOpt) if logPath != "" { options = append(options, libpod.WithLogPath(logPath)) } + if logTag != "" { + options = append(options, libpod.WithLogTag(logTag)) + } if c.LogDriver != "" { options = append(options, libpod.WithLogDriver(c.LogDriver)) diff --git a/pkg/spec/parse.go b/pkg/spec/parse.go index c2572a033..6fa0b0636 100644 --- a/pkg/spec/parse.go +++ b/pkg/spec/parse.go @@ -132,16 +132,23 @@ func validateIOpsDevice(val string) (*throttleDevice, error) { //nolint }, nil } -func getLoggingPath(opts []string) string { +// getLoggingOpts splits the path= and tag= options provided to --log-opt. +func getLoggingOpts(opts []string) (string, string) { + var path, tag string for _, opt := range opts { arr := strings.SplitN(opt, "=", 2) if len(arr) == 2 { if strings.TrimSpace(arr[0]) == "path" { - return strings.TrimSpace(arr[1]) + path = strings.TrimSpace(arr[1]) + } else if strings.TrimSpace(arr[0]) == "tag" { + tag = strings.TrimSpace(arr[1]) } } + if path != "" && tag != "" { + break + } } - return "" + return path, tag } // ParseDevice parses device mapping string to a src, dest & permissions string diff --git a/pkg/systemdgen/systemdgen.go b/pkg/systemdgen/systemdgen.go index 09d3c6fd5..b6167a23e 100644 --- a/pkg/systemdgen/systemdgen.go +++ b/pkg/systemdgen/systemdgen.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "sort" + "strings" "text/template" "time" @@ -48,6 +49,14 @@ type ContainerInfo struct { Executable string // TimeStamp at the time of creating the unit file. Will be set internally. TimeStamp string + // New controls if a new container is created or if an existing one is started. + New bool + // CreateCommand is the full command plus arguments of the process the + // container has been created with. + CreateCommand []string + // RunCommand is a post-processed variant of CreateCommand and used for + // the ExecStart field in generic unit files. + RunCommand string } var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} @@ -84,17 +93,35 @@ Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ [Service] Restart={{.RestartPolicy}} +{{- if .New}} +ExecStartPre=/usr/bin/rm -f /%t/%n-pid /%t/%n-cid +ExecStart={{.RunCommand}} +ExecStop={{.Executable}} stop --cidfile /%t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} +ExecStopPost={{.Executable}} rm -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}} +PIDFile={{.PIDFile}} +{{- end}} KillMode=none Type=forking -PIDFile={{.PIDFile}} [Install] WantedBy=multi-user.target` +// Options include different options to control the unit file generation. +type Options struct { + // When set, generate service files in the current working directory and + // return the paths to these files instead of returning all contents in one + // big string. + Files bool + // New controls if a new container is created or if an existing one is started. + New bool +} + // CreateContainerSystemdUnit creates a systemd unit file for a container. -func CreateContainerSystemdUnit(info *ContainerInfo, generateFiles bool) (string, error) { +func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, error) { if err := validateRestartPolicy(info.RestartPolicy); err != nil { return "", err } @@ -109,6 +136,36 @@ func CreateContainerSystemdUnit(info *ContainerInfo, generateFiles bool) (string info.Executable = executable } + // Assemble the ExecStart command when creating a new container. + // + // Note that we cannot catch all corner cases here such that users + // *must* manually check the generated files. A container might have + // been created via a Python script, which would certainly yield an + // invalid `info.CreateCommand`. Hence, we're doing a best effort unit + // generation and don't try aiming at completeness. + if opts.New { + // The create command must at least have three arguments: + // /usr/bin/podman run $IMAGE + index := 2 + if info.CreateCommand[1] == "container" { + index = 3 + } + if len(info.CreateCommand) < index+1 { + return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand) + } + // We're hard-coding the first four arguments and append the + // CreatCommand with a stripped command and subcomand. + command := []string{ + info.Executable, + "run", + "--conmon-pidfile", "/%t/%n-pid", + "--cidfile", "/%t/%n-cid", + } + command = append(command, info.CreateCommand[index:]...) + info.RunCommand = strings.Join(command, " ") + info.New = true + } + if info.PodmanVersion == "" { info.PodmanVersion = version.Version } @@ -131,7 +188,7 @@ func CreateContainerSystemdUnit(info *ContainerInfo, generateFiles bool) (string return "", err } - if !generateFiles { + if !opts.Files { return buf.String(), nil } diff --git a/pkg/systemdgen/systemdgen_test.go b/pkg/systemdgen/systemdgen_test.go index 1ddb0c514..e1da7e8e0 100644 --- a/pkg/systemdgen/systemdgen_test.go +++ b/pkg/systemdgen/systemdgen_test.go @@ -44,9 +44,9 @@ Documentation=man:podman-generate-systemd(1) Restart=always ExecStart=/usr/bin/podman start 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 ExecStop=/usr/bin/podman stop -t 10 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid KillMode=none Type=forking -PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid [Install] WantedBy=multi-user.target` @@ -62,9 +62,9 @@ Documentation=man:podman-generate-systemd(1) Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid KillMode=none Type=forking -PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid [Install] WantedBy=multi-user.target` @@ -84,9 +84,9 @@ After=a.service b.service c.service pod.service Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid KillMode=none Type=forking -PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid [Install] WantedBy=multi-user.target` @@ -104,9 +104,29 @@ Before=container-1.service container-2.service Restart=always ExecStart=/usr/bin/podman start jadda-jadda-infra ExecStop=/usr/bin/podman stop -t 10 jadda-jadda-infra +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +KillMode=none +Type=forking + +[Install] +WantedBy=multi-user.target` + + goodNameNew := `# jadda-jadda.service +# autogenerated by Podman CI + +[Unit] +Description=Podman jadda-jadda.service +Documentation=man:podman-generate-systemd(1) + +[Service] +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 --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN +ExecStop=/usr/bin/podman stop --cidfile /%t/%n-cid -t 42 +ExecStopPost=/usr/bin/podman rm -f --cidfile /%t/%n-cid +PIDFile=/%t/%n-pid KillMode=none Type=forking -PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid [Install] WantedBy=multi-user.target` @@ -184,16 +204,35 @@ WantedBy=multi-user.target` "", true, }, + {"good with name and generic", + 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", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, + }, + goodNameNew, + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := CreateContainerSystemdUnit(&tt.info, false) + opts := Options{ + Files: false, + New: tt.info.New, + } + got, err := CreateContainerSystemdUnit(&tt.info, opts) if (err != nil) != tt.wantErr { t.Errorf("CreateContainerSystemdUnit() error = \n%v, wantErr \n%v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("CreateContainerSystemdUnit() = \n%v, want \n%v", got, tt.want) + t.Errorf("CreateContainerSystemdUnit() = \n%v\n---------> want\n%v", got, tt.want) } }) } diff --git a/pkg/util/utils.go b/pkg/util/utils.go index c9d09b8b5..9269f6115 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -335,6 +335,13 @@ func ParseIDMapping(mode namespaces.UsernsMode, UIDMapSlice, GIDMapSlice []strin 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() @@ -352,13 +359,17 @@ func ParseIDMapping(mode namespaces.UsernsMode, UIDMapSlice, GIDMapSlice []strin options.UIDMap, options.GIDMap = nil, nil - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: uid}) + 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}) - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) + 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: gid}) + 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}) - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) + 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 diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index bc644f87c..333595a96 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -249,7 +249,7 @@ func (i *LibpodAPI) BuildImage(call iopodman.VarlinkCall, config iopodman.BuildI c := make(chan error) go func() { - err := i.Runtime.Build(getContext(), options, newPathDockerFiles...) + _, _, err := i.Runtime.Build(getContext(), options, newPathDockerFiles...) c <- err close(c) }() diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go index 91072b023..f0fef41a4 100644 --- a/test/e2e/generate_systemd_test.go +++ b/test/e2e/generate_systemd_test.go @@ -177,4 +177,32 @@ var _ = Describe("Podman generate systemd", func() { found, _ = session.GrepString("/container-foo-1.service") Expect(found).To(BeTrue()) }) + + It("podman generate systemd --new", func() { + n := podmanTest.Podman([]string{"create", "--name", "foo", "alpine", "top"}) + n.WaitWithDefaultTimeout() + Expect(n.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "--timeout", "42", "--name", "--new", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + // Grepping the output (in addition to unit tests) + found, _ := session.GrepString("# container-foo.service") + Expect(found).To(BeTrue()) + + found, _ = session.GrepString("stop --cidfile /%t/%n-cid -t 42") + Expect(found).To(BeTrue()) + }) + + It("podman generate systemd --new pod", func() { + n := podmanTest.Podman([]string{"pod", "create", "--name", "foo"}) + n.WaitWithDefaultTimeout() + Expect(n.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "--timeout", "42", "--name", "--new", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + }) + }) diff --git a/test/e2e/libpod_suite_remoteclient_test.go b/test/e2e/libpod_suite_remoteclient_test.go index 2cd485114..c87ff016a 100644 --- a/test/e2e/libpod_suite_remoteclient_test.go +++ b/test/e2e/libpod_suite_remoteclient_test.go @@ -24,7 +24,7 @@ func SkipIfRemote() { func SkipIfRootless() { if os.Geteuid() != 0 { - ginkgo.Skip("This function is not enabled for remote podman") + ginkgo.Skip("This function is not enabled for rootless podman") } } diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go index e25364695..0438a31cb 100644 --- a/test/e2e/logs_test.go +++ b/test/e2e/logs_test.go @@ -1,7 +1,9 @@ package integration import ( + "fmt" "os" + "os/exec" "strings" . "github.com/containers/libpod/test/utils" @@ -153,6 +155,23 @@ var _ = Describe("Podman logs", func() { Expect(results.ExitCode()).To(BeZero()) }) + It("podman journald logs for container with container tag", func() { + Skip("need to verify images have correct packages for journald") + logc := podmanTest.Podman([]string{"run", "--log-driver", "journald", "--log-opt=tag={{.ImageName}}", "-d", ALPINE, "sh", "-c", "echo podman; sleep 0.1; echo podman; sleep 0.1; echo podman"}) + logc.WaitWithDefaultTimeout() + Expect(logc.ExitCode()).To(Equal(0)) + cid := logc.OutputToString() + + wait := podmanTest.Podman([]string{"wait", "-l"}) + wait.WaitWithDefaultTimeout() + Expect(wait.ExitCode()).To(BeZero()) + + cmd := exec.Command("journalctl", "--no-pager", "-o", "json", "--output-fields=CONTAINER_TAG", "-u", fmt.Sprintf("libpod-conmon-%s.scope", cid)) + out, err := cmd.CombinedOutput() + Expect(err).To(BeNil()) + Expect(string(out)).To(ContainSubstring("alpine")) + }) + It("podman journald logs for container", func() { Skip("need to verify images have correct packages for journald") logc := podmanTest.Podman([]string{"run", "--log-driver", "journald", "-dt", ALPINE, "sh", "-c", "echo podman; echo podman; echo podman"}) diff --git a/vendor/github.com/google/uuid/.travis.yml b/vendor/github.com/google/uuid/.travis.yml new file mode 100644 index 000000000..d8156a60b --- /dev/null +++ b/vendor/github.com/google/uuid/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.4.3 + - 1.5.3 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 000000000..04fdf09f1 --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 000000000..b4bb97f6b --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman <borman@google.com> +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 000000000..5dc68268d --- /dev/null +++ b/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md new file mode 100644 index 000000000..9d92c11f1 --- /dev/null +++ b/vendor/github.com/google/uuid/README.md @@ -0,0 +1,19 @@ +# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on +[RFC 4122](http://tools.ietf.org/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +`go get github.com/google/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go new file mode 100644 index 000000000..fa820b9d3 --- /dev/null +++ b/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go new file mode 100644 index 000000000..5b8a4b9af --- /dev/null +++ b/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/vendor/github.com/google/uuid/go.mod b/vendor/github.com/google/uuid/go.mod new file mode 100644 index 000000000..fc84cd79d --- /dev/null +++ b/vendor/github.com/google/uuid/go.mod @@ -0,0 +1 @@ +module github.com/google/uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go new file mode 100644 index 000000000..b17461631 --- /dev/null +++ b/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) + h.Write(data) + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 000000000..7f9e0c6c0 --- /dev/null +++ b/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,37 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err == nil { + *uuid = id + } + return err +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go new file mode 100644 index 000000000..d651a2b06 --- /dev/null +++ b/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 000000000..24b78edc9 --- /dev/null +++ b/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This remvoves the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 000000000..0cbbcddbd --- /dev/null +++ b/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go new file mode 100644 index 000000000..f326b54db --- /dev/null +++ b/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go new file mode 100644 index 000000000..e6ef06cdc --- /dev/null +++ b/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go new file mode 100644 index 000000000..5ea6c7378 --- /dev/null +++ b/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 000000000..524404cc5 --- /dev/null +++ b/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,245 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +var rander = rand.Reader // random function + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if strings.ToLower(s[:9]) != "urn:uuid:" { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go new file mode 100644 index 000000000..199a1ac65 --- /dev/null +++ b/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nodeMu.Unlock() + + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + copy(uuid[10:], nodeID[:]) + + return uuid, nil +} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go new file mode 100644 index 000000000..84af91c9f --- /dev/null +++ b/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + var uuid UUID + _, err := io.ReadFull(rander, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/vendor/github.com/gorilla/schema/.travis.yml b/vendor/github.com/gorilla/schema/.travis.yml new file mode 100644 index 000000000..5f51dce4e --- /dev/null +++ b/vendor/github.com/gorilla/schema/.travis.yml @@ -0,0 +1,18 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: 1.8 + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/schema/LICENSE b/vendor/github.com/gorilla/schema/LICENSE new file mode 100644 index 000000000..0e5fb8728 --- /dev/null +++ b/vendor/github.com/gorilla/schema/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/schema/README.md b/vendor/github.com/gorilla/schema/README.md new file mode 100644 index 000000000..aefdd6699 --- /dev/null +++ b/vendor/github.com/gorilla/schema/README.md @@ -0,0 +1,90 @@ +schema +====== +[![GoDoc](https://godoc.org/github.com/gorilla/schema?status.svg)](https://godoc.org/github.com/gorilla/schema) [![Build Status](https://travis-ci.org/gorilla/schema.png?branch=master)](https://travis-ci.org/gorilla/schema) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/schema/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/schema?badge) + + +Package gorilla/schema converts structs to and from form values. + +## Example + +Here's a quick example: we parse POST form values and then decode them into a struct: + +```go +// Set a Decoder instance as a package global, because it caches +// meta-data about structs, and an instance can be shared safely. +var decoder = schema.NewDecoder() + +type Person struct { + Name string + Phone string +} + +func MyHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + // Handle error + } + + var person Person + + // r.PostForm is a map of our POST form values + err = decoder.Decode(&person, r.PostForm) + if err != nil { + // Handle error + } + + // Do something with person.Name or person.Phone +} +``` + +Conversely, contents of a struct can be encoded into form values. Here's a variant of the previous example using the Encoder: + +```go +var encoder = schema.NewEncoder() + +func MyHttpRequest() { + person := Person{"Jane Doe", "555-5555"} + form := url.Values{} + + err := encoder.Encode(person, form) + + if err != nil { + // Handle error + } + + // Use form values, for example, with an http client + client := new(http.Client) + res, err := client.PostForm("http://my-api.test", form) +} + +``` + +To define custom names for fields, use a struct tag "schema". To not populate certain fields, use a dash for the name and it will be ignored: + +```go +type Person struct { + Name string `schema:"name,required"` // custom name, must be supplied + Phone string `schema:"phone"` // custom name + Admin bool `schema:"-"` // this field is never set +} +``` + +The supported field types in the struct are: + +* bool +* float variants (float32, float64) +* int variants (int, int8, int16, int32, int64) +* string +* uint variants (uint, uint8, uint16, uint32, uint64) +* struct +* a pointer to one of the above types +* a slice or a pointer to a slice of one of the above types + +Unsupported types are simply ignored, however custom types can be registered to be converted. + +More examples are available on the Gorilla website: https://www.gorillatoolkit.org/pkg/schema + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/schema/cache.go b/vendor/github.com/gorilla/schema/cache.go new file mode 100644 index 000000000..0746c1202 --- /dev/null +++ b/vendor/github.com/gorilla/schema/cache.go @@ -0,0 +1,305 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "errors" + "reflect" + "strconv" + "strings" + "sync" +) + +var invalidPath = errors.New("schema: invalid path") + +// newCache returns a new cache. +func newCache() *cache { + c := cache{ + m: make(map[reflect.Type]*structInfo), + regconv: make(map[reflect.Type]Converter), + tag: "schema", + } + return &c +} + +// cache caches meta-data about a struct. +type cache struct { + l sync.RWMutex + m map[reflect.Type]*structInfo + regconv map[reflect.Type]Converter + tag string +} + +// registerConverter registers a converter function for a custom type. +func (c *cache) registerConverter(value interface{}, converterFunc Converter) { + c.regconv[reflect.TypeOf(value)] = converterFunc +} + +// parsePath parses a path in dotted notation verifying that it is a valid +// path to a struct field. +// +// It returns "path parts" which contain indices to fields to be used by +// reflect.Value.FieldByString(). Multiple parts are required for slices of +// structs. +func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { + var struc *structInfo + var field *fieldInfo + var index64 int64 + var err error + parts := make([]pathPart, 0) + path := make([]string, 0) + keys := strings.Split(p, ".") + for i := 0; i < len(keys); i++ { + if t.Kind() != reflect.Struct { + return nil, invalidPath + } + if struc = c.get(t); struc == nil { + return nil, invalidPath + } + if field = struc.get(keys[i]); field == nil { + return nil, invalidPath + } + // Valid field. Append index. + path = append(path, field.name) + if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { + // Parse a special case: slices of structs. + // i+1 must be the slice index. + // + // Now that struct can implements TextUnmarshaler interface, + // we don't need to force the struct's fields to appear in the path. + // So checking i+2 is not necessary anymore. + i++ + if i+1 > len(keys) { + return nil, invalidPath + } + if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { + return nil, invalidPath + } + parts = append(parts, pathPart{ + path: path, + field: field, + index: int(index64), + }) + path = make([]string, 0) + + // Get the next struct type, dropping ptrs. + if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + if t.Kind() == reflect.Slice { + t = t.Elem() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + } + } else if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + } + // Add the remaining. + parts = append(parts, pathPart{ + path: path, + field: field, + index: -1, + }) + return parts, nil +} + +// get returns a cached structInfo, creating it if necessary. +func (c *cache) get(t reflect.Type) *structInfo { + c.l.RLock() + info := c.m[t] + c.l.RUnlock() + if info == nil { + info = c.create(t, "") + c.l.Lock() + c.m[t] = info + c.l.Unlock() + } + return info +} + +// create creates a structInfo with meta-data about a struct. +func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { + info := &structInfo{} + var anonymousInfos []*structInfo + for i := 0; i < t.NumField(); i++ { + if f := c.createField(t.Field(i), parentAlias); f != nil { + info.fields = append(info.fields, f) + if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { + anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) + } + } + } + for i, a := range anonymousInfos { + others := []*structInfo{info} + others = append(others, anonymousInfos[:i]...) + others = append(others, anonymousInfos[i+1:]...) + for _, f := range a.fields { + if !containsAlias(others, f.alias) { + info.fields = append(info.fields, f) + } + } + } + return info +} + +// createField creates a fieldInfo for the given field. +func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { + alias, options := fieldAlias(field, c.tag) + if alias == "-" { + // Ignore this field. + return nil + } + canonicalAlias := alias + if parentAlias != "" { + canonicalAlias = parentAlias + "." + alias + } + // Check if the type is supported and don't cache it if not. + // First let's get the basic type. + isSlice, isStruct := false, false + ft := field.Type + m := isTextUnmarshaler(reflect.Zero(ft)) + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + if isSlice = ft.Kind() == reflect.Slice; isSlice { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if ft.Kind() == reflect.Array { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if isStruct = ft.Kind() == reflect.Struct; !isStruct { + if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { + // Type is not supported. + return nil + } + } + + return &fieldInfo{ + typ: field.Type, + name: field.Name, + alias: alias, + canonicalAlias: canonicalAlias, + unmarshalerInfo: m, + isSliceOfStructs: isSlice && isStruct, + isAnonymous: field.Anonymous, + isRequired: options.Contains("required"), + } +} + +// converter returns the converter for a type. +func (c *cache) converter(t reflect.Type) Converter { + return c.regconv[t] +} + +// ---------------------------------------------------------------------------- + +type structInfo struct { + fields []*fieldInfo +} + +func (i *structInfo) get(alias string) *fieldInfo { + for _, field := range i.fields { + if strings.EqualFold(field.alias, alias) { + return field + } + } + return nil +} + +func containsAlias(infos []*structInfo, alias string) bool { + for _, info := range infos { + if info.get(alias) != nil { + return true + } + } + return false +} + +type fieldInfo struct { + typ reflect.Type + // name is the field name in the struct. + name string + alias string + // canonicalAlias is almost the same as the alias, but is prefixed with + // an embedded struct field alias in dotted notation if this field is + // promoted from the struct. + // For instance, if the alias is "N" and this field is an embedded field + // in a struct "X", canonicalAlias will be "X.N". + canonicalAlias string + // unmarshalerInfo contains information regarding the + // encoding.TextUnmarshaler implementation of the field type. + unmarshalerInfo unmarshaler + // isSliceOfStructs indicates if the field type is a slice of structs. + isSliceOfStructs bool + // isAnonymous indicates whether the field is embedded in the struct. + isAnonymous bool + isRequired bool +} + +func (f *fieldInfo) paths(prefix string) []string { + if f.alias == f.canonicalAlias { + return []string{prefix + f.alias} + } + return []string{prefix + f.alias, prefix + f.canonicalAlias} +} + +type pathPart struct { + field *fieldInfo + path []string // path to the field: walks structs using field names. + index int // struct index in slices of structs. +} + +// ---------------------------------------------------------------------------- + +func indirectType(typ reflect.Type) reflect.Type { + if typ.Kind() == reflect.Ptr { + return typ.Elem() + } + return typ +} + +// fieldAlias parses a field tag to get a field alias. +func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { + if tag := field.Tag.Get(tagName); tag != "" { + alias, options = parseTag(tag) + } + if alias == "" { + alias = field.Name + } + return alias, options +} + +// tagOptions is the string following a comma in a struct field's tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/vendor/github.com/gorilla/schema/converter.go b/vendor/github.com/gorilla/schema/converter.go new file mode 100644 index 000000000..4f2116a15 --- /dev/null +++ b/vendor/github.com/gorilla/schema/converter.go @@ -0,0 +1,145 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "reflect" + "strconv" +) + +type Converter func(string) reflect.Value + +var ( + invalidValue = reflect.Value{} + boolType = reflect.Bool + float32Type = reflect.Float32 + float64Type = reflect.Float64 + intType = reflect.Int + int8Type = reflect.Int8 + int16Type = reflect.Int16 + int32Type = reflect.Int32 + int64Type = reflect.Int64 + stringType = reflect.String + uintType = reflect.Uint + uint8Type = reflect.Uint8 + uint16Type = reflect.Uint16 + uint32Type = reflect.Uint32 + uint64Type = reflect.Uint64 +) + +// Default converters for basic types. +var builtinConverters = map[reflect.Kind]Converter{ + boolType: convertBool, + float32Type: convertFloat32, + float64Type: convertFloat64, + intType: convertInt, + int8Type: convertInt8, + int16Type: convertInt16, + int32Type: convertInt32, + int64Type: convertInt64, + stringType: convertString, + uintType: convertUint, + uint8Type: convertUint8, + uint16Type: convertUint16, + uint32Type: convertUint32, + uint64Type: convertUint64, +} + +func convertBool(value string) reflect.Value { + if value == "on" { + return reflect.ValueOf(true) + } else if v, err := strconv.ParseBool(value); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertFloat32(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 32); err == nil { + return reflect.ValueOf(float32(v)) + } + return invalidValue +} + +func convertFloat64(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertInt(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 0); err == nil { + return reflect.ValueOf(int(v)) + } + return invalidValue +} + +func convertInt8(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 8); err == nil { + return reflect.ValueOf(int8(v)) + } + return invalidValue +} + +func convertInt16(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 16); err == nil { + return reflect.ValueOf(int16(v)) + } + return invalidValue +} + +func convertInt32(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 32); err == nil { + return reflect.ValueOf(int32(v)) + } + return invalidValue +} + +func convertInt64(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertString(value string) reflect.Value { + return reflect.ValueOf(value) +} + +func convertUint(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 0); err == nil { + return reflect.ValueOf(uint(v)) + } + return invalidValue +} + +func convertUint8(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 8); err == nil { + return reflect.ValueOf(uint8(v)) + } + return invalidValue +} + +func convertUint16(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 16); err == nil { + return reflect.ValueOf(uint16(v)) + } + return invalidValue +} + +func convertUint32(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 32); err == nil { + return reflect.ValueOf(uint32(v)) + } + return invalidValue +} + +func convertUint64(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} diff --git a/vendor/github.com/gorilla/schema/decoder.go b/vendor/github.com/gorilla/schema/decoder.go new file mode 100644 index 000000000..5afbd921f --- /dev/null +++ b/vendor/github.com/gorilla/schema/decoder.go @@ -0,0 +1,504 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strings" +) + +// NewDecoder returns a new Decoder. +func NewDecoder() *Decoder { + return &Decoder{cache: newCache()} +} + +// Decoder decodes values from a map[string][]string to a struct. +type Decoder struct { + cache *cache + zeroEmpty bool + ignoreUnknownKeys bool +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (d *Decoder) SetAliasTag(tag string) { + d.cache.tag = tag +} + +// ZeroEmpty controls the behaviour when the decoder encounters empty values +// in a map. +// If z is true and a key in the map has the empty string as a value +// then the corresponding struct field is set to the zero value. +// If z is false then empty strings are ignored. +// +// The default value is false, that is empty values do not change +// the value of the struct field. +func (d *Decoder) ZeroEmpty(z bool) { + d.zeroEmpty = z +} + +// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown +// keys in the map. +// If i is true and an unknown field is encountered, it is ignored. This is +// similar to how unknown keys are handled by encoding/json. +// If i is false then Decode will return an error. Note that any valid keys +// will still be decoded in to the target struct. +// +// To preserve backwards compatibility, the default value is false. +func (d *Decoder) IgnoreUnknownKeys(i bool) { + d.ignoreUnknownKeys = i +} + +// RegisterConverter registers a converter function for a custom type. +func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { + d.cache.registerConverter(value, converterFunc) +} + +// Decode decodes a map[string][]string to a struct. +// +// The first parameter must be a pointer to a struct. +// +// The second parameter is a map, typically url.Values from an HTTP request. +// Keys are "paths" in dotted notation to the struct fields and nested structs. +// +// See the package documentation for a full explanation of the mechanics. +func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return errors.New("schema: interface must be a pointer to struct") + } + v = v.Elem() + t := v.Type() + errors := MultiError{} + for path, values := range src { + if parts, err := d.cache.parsePath(path, t); err == nil { + if err = d.decode(v, path, parts, values); err != nil { + errors[path] = err + } + } else if !d.ignoreUnknownKeys { + errors[path] = UnknownKeyError{Key: path} + } + } + errors.merge(d.checkRequired(t, src)) + if len(errors) > 0 { + return errors + } + return nil +} + +// checkRequired checks whether required fields are empty +// +// check type t recursively if t has struct fields. +// +// src is the source map for decoding, we use it here to see if those required fields are included in src +func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { + m, errs := d.findRequiredFields(t, "", "") + for key, fields := range m { + if isEmptyFields(fields, src) { + errs[key] = EmptyFieldError{Key: key} + } + } + return errs +} + +// findRequiredFields recursively searches the struct type t for required fields. +// +// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation +// for nested struct fields. canonicalPrefix is a complete path which never omits +// any embedded struct fields. searchPrefix is a user-friendly path which may omit +// some embedded struct fields to point promoted fields. +func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { + struc := d.cache.get(t) + if struc == nil { + // unexpect, cache.get never return nil + return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} + } + + m := map[string][]fieldWithPrefix{} + errs := MultiError{} + for _, f := range struc.fields { + if f.typ.Kind() == reflect.Struct { + fcprefix := canonicalPrefix + f.canonicalAlias + "." + for _, fspath := range f.paths(searchPrefix) { + fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") + for key, fields := range fm { + m[key] = append(m[key], fields...) + } + errs.merge(ferrs) + } + } + if f.isRequired { + key := canonicalPrefix + f.canonicalAlias + m[key] = append(m[key], fieldWithPrefix{ + fieldInfo: f, + prefix: searchPrefix, + }) + } + } + return m, errs +} + +type fieldWithPrefix struct { + *fieldInfo + prefix string +} + +// isEmptyFields returns true if all of specified fields are empty. +func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { + for _, f := range fields { + for _, path := range f.paths(f.prefix) { + if !isEmpty(f.typ, src[path]) { + return false + } + } + } + return true +} + +// isEmpty returns true if value is empty for specific type +func isEmpty(t reflect.Type, value []string) bool { + if len(value) == 0 { + return true + } + switch t.Kind() { + case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: + return len(value[0]) == 0 + } + return false +} + +// decode fills a struct field using a parsed path. +func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { + // Get the field walking the struct fields by index. + for _, name := range parts[0].path { + if v.Type().Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + v = v.FieldByName(name) + } + // Don't even bother for unexported fields. + if !v.CanSet() { + return nil + } + + // Dereference if needed. + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + if v.IsNil() { + v.Set(reflect.New(t)) + } + v = v.Elem() + } + + // Slice of structs. Let's go recursive. + if len(parts) > 1 { + idx := parts[0].index + if v.IsNil() || v.Len() < idx+1 { + value := reflect.MakeSlice(t, idx+1, idx+1) + if v.Len() < idx+1 { + // Resize it. + reflect.Copy(value, v) + } + v.Set(value) + } + return d.decode(v.Index(idx), path, parts[1:], values) + } + + // Get the converter early in case there is one for a slice type. + conv := d.cache.converter(t) + m := isTextUnmarshaler(v) + if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { + var items []reflect.Value + elemT := t.Elem() + isPtrElem := elemT.Kind() == reflect.Ptr + if isPtrElem { + elemT = elemT.Elem() + } + + // Try to get a converter for the element type. + conv := d.cache.converter(elemT) + if conv == nil { + conv = builtinConverters[elemT.Kind()] + if conv == nil { + // As we are not dealing with slice of structs here, we don't need to check if the type + // implements TextUnmarshaler interface + return fmt.Errorf("schema: converter not found for %v", elemT) + } + } + + for key, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if m.IsValid { + u := reflect.New(elemT) + if m.IsSliceElementPtr { + u = reflect.New(reflect.PtrTo(elemT).Elem()) + } + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: key, + Err: err, + } + } + if m.IsSliceElementPtr { + items = append(items, u.Elem().Addr()) + } else if u.Kind() == reflect.Ptr { + items = append(items, u.Elem()) + } else { + items = append(items, u) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + if strings.Contains(value, ",") { + values := strings.Split(value, ",") + for _, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } + value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) + v.Set(value) + } else { + val := "" + // Use the last value provided if any values were provided + if len(values) > 0 { + val = values[len(values)-1] + } + + if conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else if m.IsValid { + if m.IsPtr { + u := reflect.New(v.Type()) + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + v.Set(reflect.Indirect(u)) + } else { + // If the value implements the encoding.TextUnmarshaler interface + // apply UnmarshalText as the converter + if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + } + } else if val == "" { + if d.zeroEmpty { + v.Set(reflect.Zero(t)) + } + } else if conv := builtinConverters[t.Kind()]; conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else { + return fmt.Errorf("schema: converter not found for %v", t) + } + } + return nil +} + +func isTextUnmarshaler(v reflect.Value) unmarshaler { + // Create a new unmarshaller instance + m := unmarshaler{} + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // As the UnmarshalText function should be applied to the pointer of the + // type, we check that type to see if it implements the necessary + // method. + if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { + m.IsPtr = true + return m + } + + // if v is []T or *[]T create new T + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + // Check if the slice implements encoding.TextUnmarshaller + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // If t is a pointer slice, check if its elements implement + // encoding.TextUnmarshaler + m.IsSliceElement = true + if t = t.Elem(); t.Kind() == reflect.Ptr { + t = reflect.PtrTo(t.Elem()) + v = reflect.Zero(t) + m.IsSliceElementPtr = true + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m + } + } + + v = reflect.New(t) + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m +} + +// TextUnmarshaler helpers ---------------------------------------------------- +// unmarshaller contains information about a TextUnmarshaler type +type unmarshaler struct { + Unmarshaler encoding.TextUnmarshaler + // IsValid indicates whether the resolved type indicated by the other + // flags implements the encoding.TextUnmarshaler interface. + IsValid bool + // IsPtr indicates that the resolved type is the pointer of the original + // type. + IsPtr bool + // IsSliceElement indicates that the resolved type is a slice element of + // the original type. + IsSliceElement bool + // IsSliceElementPtr indicates that the resolved type is a pointer to a + // slice element of the original type. + IsSliceElementPtr bool +} + +// Errors --------------------------------------------------------------------- + +// ConversionError stores information about a failed conversion. +type ConversionError struct { + Key string // key from the source map. + Type reflect.Type // expected type of elem + Index int // index for multi-value fields; -1 for single-value fields. + Err error // low-level error (when it exists) +} + +func (e ConversionError) Error() string { + var output string + + if e.Index < 0 { + output = fmt.Sprintf("schema: error converting value for %q", e.Key) + } else { + output = fmt.Sprintf("schema: error converting value for index %d of %q", + e.Index, e.Key) + } + + if e.Err != nil { + output = fmt.Sprintf("%s. Details: %s", output, e.Err) + } + + return output +} + +// UnknownKeyError stores information about an unknown key in the source map. +type UnknownKeyError struct { + Key string // key from the source map. +} + +func (e UnknownKeyError) Error() string { + return fmt.Sprintf("schema: invalid path %q", e.Key) +} + +// EmptyFieldError stores information about an empty required field. +type EmptyFieldError struct { + Key string // required key in the source map. +} + +func (e EmptyFieldError) Error() string { + return fmt.Sprintf("%v is empty", e.Key) +} + +// MultiError stores multiple decoding errors. +// +// Borrowed from the App Engine SDK. +type MultiError map[string]error + +func (e MultiError) Error() string { + s := "" + for _, err := range e { + s = err.Error() + break + } + switch len(e) { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) +} + +func (e MultiError) merge(errors MultiError) { + for key, err := range errors { + if e[key] == nil { + e[key] = err + } + } +} diff --git a/vendor/github.com/gorilla/schema/doc.go b/vendor/github.com/gorilla/schema/doc.go new file mode 100644 index 000000000..aae9f33f9 --- /dev/null +++ b/vendor/github.com/gorilla/schema/doc.go @@ -0,0 +1,148 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package gorilla/schema fills a struct with form values. + +The basic usage is really simple. Given this struct: + + type Person struct { + Name string + Phone string + } + +...we can fill it passing a map to the Decode() function: + + values := map[string][]string{ + "Name": {"John"}, + "Phone": {"999-999-999"}, + } + person := new(Person) + decoder := schema.NewDecoder() + decoder.Decode(person, values) + +This is just a simple example and it doesn't make a lot of sense to create +the map manually. Typically it will come from a http.Request object and +will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + + if err != nil { + // Handle error + } + + decoder := schema.NewDecoder() + // r.PostForm is a map of our POST form values + err := decoder.Decode(person, r.PostForm) + + if err != nil { + // Handle error + } + + // Do something with person.Name or person.Phone + } + +Note: it is a good idea to set a Decoder instance as a package global, +because it caches meta-data about structs, and an instance can be shared safely: + + var decoder = schema.NewDecoder() + +To define custom names for fields, use a struct tag "schema". To not populate +certain fields, use a dash for the name and it will be ignored: + + type Person struct { + Name string `schema:"name"` // custom name + Phone string `schema:"phone"` // custom name + Admin bool `schema:"-"` // this field is never set + } + +The supported field types in the destination struct are: + + * bool + * float variants (float32, float64) + * int variants (int, int8, int16, int32, int64) + * string + * uint variants (uint, uint8, uint16, uint32, uint64) + * struct + * a pointer to one of the above types + * a slice or a pointer to a slice of one of the above types + +Non-supported types are simply ignored, however custom types can be registered +to be converted. + +To fill nested structs, keys must use a dotted notation as the "path" for the +field. So for example, to fill the struct Person below: + + type Phone struct { + Label string + Number string + } + + type Person struct { + Name string + Phone Phone + } + +...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". +This means that an HTML form to fill a Person struct must look like this: + + <form> + <input type="text" name="Name"> + <input type="text" name="Phone.Label"> + <input type="text" name="Phone.Number"> + </form> + +Single values are filled using the first value for a key from the source map. +Slices are filled using all values for a key from the source map. So to fill +a Person with multiple Phone values, like: + + type Person struct { + Name string + Phones []Phone + } + +...an HTML form that accepts three Phone values would look like this: + + <form> + <input type="text" name="Name"> + <input type="text" name="Phones.0.Label"> + <input type="text" name="Phones.0.Number"> + <input type="text" name="Phones.1.Label"> + <input type="text" name="Phones.1.Number"> + <input type="text" name="Phones.2.Label"> + <input type="text" name="Phones.2.Number"> + </form> + +Notice that only for slices of structs the slice index is required. +This is needed for disambiguation: if the nested struct also had a slice +field, we could not translate multiple values to it if we did not use an +index for the parent struct. + +There's also the possibility to create a custom type that implements the +TextUnmarshaler interface, and in this case there's no need to register +a converter, like: + + type Person struct { + Emails []Email + } + + type Email struct { + *mail.Address + } + + func (e *Email) UnmarshalText(text []byte) (err error) { + e.Address, err = mail.ParseAddress(string(text)) + return + } + +...an HTML form that accepts three Email values would look like this: + + <form> + <input type="email" name="Emails.0"> + <input type="email" name="Emails.1"> + <input type="email" name="Emails.2"> + </form> +*/ +package schema diff --git a/vendor/github.com/gorilla/schema/encoder.go b/vendor/github.com/gorilla/schema/encoder.go new file mode 100644 index 000000000..bf1d511e6 --- /dev/null +++ b/vendor/github.com/gorilla/schema/encoder.go @@ -0,0 +1,195 @@ +package schema + +import ( + "errors" + "fmt" + "reflect" + "strconv" +) + +type encoderFunc func(reflect.Value) string + +// Encoder encodes values from a struct into url.Values. +type Encoder struct { + cache *cache + regenc map[reflect.Type]encoderFunc +} + +// NewEncoder returns a new Encoder with defaults. +func NewEncoder() *Encoder { + return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} +} + +// Encode encodes a struct into map[string][]string. +// +// Intended for use with url.Values. +func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { + v := reflect.ValueOf(src) + + return e.encode(v, dst) +} + +// RegisterEncoder registers a converter for encoding a custom type. +func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { + e.regenc[reflect.TypeOf(value)] = encoder +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (e *Encoder) SetAliasTag(tag string) { + e.cache.tag = tag +} + +// isValidStructPointer test if input value is a valid struct pointer. +func isValidStructPointer(v reflect.Value) bool { + return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func: + case reflect.Map, reflect.Slice: + return v.IsNil() || v.Len() == 0 + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + +func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return errors.New("schema: interface must be a struct") + } + t := v.Type() + + errors := MultiError{} + + for i := 0; i < v.NumField(); i++ { + name, opts := fieldAlias(t.Field(i), e.cache.tag) + if name == "-" { + continue + } + + // Encode struct pointer types if the field is a valid pointer and a struct. + if isValidStructPointer(v.Field(i)) { + e.encode(v.Field(i).Elem(), dst) + continue + } + + encFunc := typeEncoder(v.Field(i).Type(), e.regenc) + + // Encode non-slice types and custom implementations immediately. + if encFunc != nil { + value := encFunc(v.Field(i)) + if opts.Contains("omitempty") && isZero(v.Field(i)) { + continue + } + + dst[name] = append(dst[name], value) + continue + } + + if v.Field(i).Type().Kind() == reflect.Struct { + e.encode(v.Field(i), dst) + continue + } + + if v.Field(i).Type().Kind() == reflect.Slice { + encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) + } + + if encFunc == nil { + errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) + continue + } + + // Encode a slice. + if v.Field(i).Len() == 0 && opts.Contains("omitempty") { + continue + } + + dst[name] = []string{} + for j := 0; j < v.Field(i).Len(); j++ { + dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) + } + } + + if len(errors) > 0 { + return errors + } + return nil +} + +func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { + if f, ok := reg[t]; ok { + return f + } + + switch t.Kind() { + case reflect.Bool: + return encodeBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return encodeInt + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return encodeUint + case reflect.Float32: + return encodeFloat32 + case reflect.Float64: + return encodeFloat64 + case reflect.Ptr: + f := typeEncoder(t.Elem(), reg) + return func(v reflect.Value) string { + if v.IsNil() { + return "null" + } + return f(v.Elem()) + } + case reflect.String: + return encodeString + default: + return nil + } +} + +func encodeBool(v reflect.Value) string { + return strconv.FormatBool(v.Bool()) +} + +func encodeInt(v reflect.Value) string { + return strconv.FormatInt(int64(v.Int()), 10) +} + +func encodeUint(v reflect.Value) string { + return strconv.FormatUint(uint64(v.Uint()), 10) +} + +func encodeFloat(v reflect.Value, bits int) string { + return strconv.FormatFloat(v.Float(), 'f', 6, bits) +} + +func encodeFloat32(v reflect.Value) string { + return encodeFloat(v, 32) +} + +func encodeFloat64(v reflect.Value) string { + return encodeFloat(v, 64) +} + +func encodeString(v reflect.Value) string { + return v.String() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5a2d4ab81..3f6454f9d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -286,8 +286,12 @@ github.com/golang/protobuf/ptypes/timestamp github.com/google/gofuzz # github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf github.com/google/shlex +# github.com/google/uuid v1.1.1 +github.com/google/uuid # github.com/gorilla/mux v1.7.3 github.com/gorilla/mux +# github.com/gorilla/schema v1.1.0 +github.com/gorilla/schema # github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/errwrap # github.com/hashicorp/go-multierror v1.0.0 |