diff options
-rw-r--r-- | .cirrus.yml | 59 | ||||
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | cmd/podman/cliconfig/config.go | 6 | ||||
-rw-r--r-- | cmd/podman/commands.go | 3 | ||||
-rw-r--r-- | cmd/podman/service.go | 154 | ||||
-rw-r--r-- | cmd/podman/service_dummy.go | 11 | ||||
-rw-r--r-- | cmd/podman/varlink.go | 2 | ||||
-rw-r--r-- | cmd/service/main.go | 55 | ||||
-rw-r--r-- | commands.md | 1 | ||||
-rw-r--r-- | completions/bash/podman | 13 | ||||
-rw-r--r-- | docs/source/markdown/podman-service.1.md | 47 | ||||
-rw-r--r-- | docs/source/markdown/podman.1.md | 1 | ||||
-rw-r--r-- | pkg/adapter/client.go | 2 | ||||
-rw-r--r-- | pkg/adapter/client_config.go | 7 | ||||
-rw-r--r-- | pkg/api/handlers/images.go | 17 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/containers.go | 16 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/pods.go | 17 | ||||
-rw-r--r-- | pkg/api/handlers/swagger.go | 4 | ||||
-rw-r--r-- | pkg/api/handlers/utils/errors.go | 5 | ||||
-rw-r--r-- | pkg/api/server/listener_api.go | 31 | ||||
-rw-r--r-- | pkg/api/server/register_containers.go | 25 | ||||
-rw-r--r-- | pkg/api/server/register_images.go | 12 | ||||
-rw-r--r-- | pkg/api/server/register_pods.go | 3 | ||||
-rw-r--r-- | pkg/api/server/server.go | 50 | ||||
-rw-r--r-- | pkg/bindings/containers.go | 8 |
25 files changed, 433 insertions, 126 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index fb6dd4cdb..e1810fab6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -514,6 +514,7 @@ special_testing_cgroupv2_task: always: <<: *standardlogs + special_testing_endpoint_task: depends_on: @@ -561,7 +562,8 @@ test_build_cache_images_task: only_if: >- $CIRRUS_BRANCH != $DEST_BRANCH && - $CIRRUS_CHANGE_MESSAGE =~ '.*CI:IMG.*' + $CIRRUS_CHANGE_MESSAGE =~ '.*CI:IMG.*' && + $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:DOCS.*' depends_on: - "gating" @@ -593,7 +595,8 @@ verify_test_built_images_task: only_if: >- $CIRRUS_BRANCH != $DEST_BRANCH && - $CIRRUS_CHANGE_MESSAGE =~ '.*CI:IMG.*' + $CIRRUS_CHANGE_MESSAGE =~ '.*CI:IMG.*' && + $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:DOCS.*' depends_on: @@ -637,33 +640,38 @@ verify_test_built_images_task: always: <<: *standardlogs - -upload_snap_task: - - # Only when PR or branch is merged into master - only_if: $CIRRUS_BRANCH == $DEST_BRANCH - - depends_on: - - "test_building_snap" - - container: - image: yakshaveinc/snapcraft:core18 - - env: - SNAPCRAFT_LOGIN: ENCRYPTED[d8e82eb31c6372fec07f405f413d57806026b1a9f8400033531ebcd54d6750a5e4a8b1f68e3ec65c98c65e0d9b2a6a75] - snapcraft_login_file: - path: /root/.snapcraft/login.cfg - variable_name: SNAPCRAFT_LOGIN - snapcraft_script: - - 'apt-get -y update' - - 'snapcraft login --with "/root/.snapcraft/login.cfg"' - - 'cd contrib/snapcraft && snapcraft && snapcraft push *.snap --release edge' + #upload_snap_task: + # only_if: >- + # $CIRRUS_BRANCH != $DEST_BRANCH && + # $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:IMG.*' && + # $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:DOCS.*' + # + # # Only when PR or branch is merged into master + # + # depends_on: + # - "test_building_snap" + # + # container: + # image: yakshaveinc/snapcraft:core18 + # + # env: + # SNAPCRAFT_LOGIN: ENCRYPTED[d8e82eb31c6372fec07f405f413d57806026b1a9f8400033531ebcd54d6750a5e4a8b1f68e3ec65c98c65e0d9b2a6a75] + # snapcraft_login_file: + # path: /root/.snapcraft/login.cfg + # variable_name: SNAPCRAFT_LOGIN + # snapcraft_script: + # - 'apt-get -y update' + # - 'snapcraft login --with "/root/.snapcraft/login.cfg"' + # - 'cd contrib/snapcraft && snapcraft && snapcraft push *.snap --release edge' docs_task: - # Only run this for PRs on mention, but always run after merge - only_if: $CIRRUS_CHANGE_MESSAGE =~ '.*CI:DOCS.*' || $CIRRUS_BRANCH == $DEST_BRANCH + # Only run this for PRs on mention, and after merge + only_if: >- + $CIRRUS_BRANCH == $DEST_BRANCH && + $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:IMG.*' && + $CIRRUS_CHANGE_MESSAGE =~ '.*CI:DOCS.*' depends_on: - "gating" @@ -697,7 +705,6 @@ success_task: - "special_testing_endpoint" - "test_build_cache_images" - "test_building_snap" - - "upload_snap" - "verify_test_built_images" - "docs" @@ -199,14 +199,6 @@ 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)) @@ -328,7 +320,7 @@ system.test-binary: .install.ginkgo vagrant-check: BOX=$(BOX) sh ./vagrant.sh -binaries: varlink_generate podman podman-remote service ## Build podman +binaries: varlink_generate podman podman-remote ## Build podman install.catatonit: ./hack/install_catatonit.sh diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index b261599e6..6bc8aa4a3 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -599,6 +599,12 @@ type VarlinkValues struct { Timeout int64 } +type ServiceValues struct { + PodmanCommand + Varlink bool + Timeout int64 +} + type SetTrustValues struct { PodmanCommand PolicyPath string diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index 31f1b3ba4..ebd7aeb0c 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -26,6 +26,9 @@ func getMainCommands() []*cobra.Command { if len(_varlinkCommand.Use) > 0 { rootCommands = append(rootCommands, _varlinkCommand) } + if len(_serviceCommand.Use) > 0 { + rootCommands = append(rootCommands, _serviceCommand) + } return rootCommands } diff --git a/cmd/podman/service.go b/cmd/podman/service.go new file mode 100644 index 000000000..6e2b4a366 --- /dev/null +++ b/cmd/podman/service.go @@ -0,0 +1,154 @@ +// +build varlink,!remoteclient + +package main + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/cmd/podman/libpodruntime" + iopodman "github.com/containers/libpod/cmd/podman/varlink" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" + api "github.com/containers/libpod/pkg/api/server" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" + "github.com/containers/libpod/pkg/varlinkapi" + "github.com/containers/libpod/version" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/varlink/go/varlink" +) + +var ( + serviceCommand cliconfig.ServiceValues + serviceDescription = `Run an API service + +Enable a listening service for API access to Podman commands. +` + + _serviceCommand = &cobra.Command{ + Use: "service [flags] [URI]", + Short: "Run API service", + Long: serviceDescription, + RunE: func(cmd *cobra.Command, args []string) error { + serviceCommand.InputArgs = args + serviceCommand.GlobalFlags = MainGlobalOpts + return serviceCmd(&serviceCommand) + }, + } +) + +func init() { + serviceCommand.Command = _serviceCommand + serviceCommand.SetHelpTemplate(HelpTemplate()) + serviceCommand.SetUsageTemplate(UsageTemplate()) + flags := serviceCommand.Flags() + flags.Int64VarP(&serviceCommand.Timeout, "timeout", "t", 1000, "Time until the service session expires in milliseconds. Use 0 to disable the timeout") + flags.BoolVar(&serviceCommand.Varlink, "varlink", false, "Use legacy varlink service instead of REST") +} + +func serviceCmd(c *cliconfig.ServiceValues) error { + // For V2, default to the REST socket + apiURI := adapter.DefaultAPIAddress + if c.Varlink { + apiURI = adapter.DefaultVarlinkAddress + } + + if rootless.IsRootless() { + xdg, err := util.GetRuntimeDir() + if err != nil { + return err + } + socketName := "podman.sock" + if c.Varlink { + socketName = "io.podman" + } + socketDir := filepath.Join(xdg, "podman", socketName) + if _, err := os.Stat(filepath.Dir(socketDir)); err != nil { + if os.IsNotExist(err) { + if err := os.Mkdir(filepath.Dir(socketDir), 0755); err != nil { + return err + } + } else { + return err + } + } + apiURI = fmt.Sprintf("unix:%s", socketDir) + } + + if len(c.InputArgs) > 0 { + apiURI = c.InputArgs[0] + } + + logrus.Infof("using API endpoint: %s", apiURI) + + // Create a single runtime api consumption + runtime, err := libpodruntime.GetRuntimeDisableFDs(getContext(), &c.PodmanCommand) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.DeferredShutdown(false) + + timeout := time.Duration(c.Timeout) * time.Millisecond + if c.Varlink { + return runVarlink(runtime, apiURI, timeout, c) + } + return runREST(runtime, apiURI, timeout) +} + +func runREST(r *libpod.Runtime, uri string, timeout time.Duration) error { + logrus.Warn("This function is EXPERIMENTAL") + fmt.Println("This function is EXPERIMENTAL.") + fields := strings.Split(uri, ":") + if len(fields) == 1 { + return errors.Errorf("%s is an invalid socket destination", uri) + } + address := strings.Join(fields[1:], ":") + l, err := net.Listen(fields[0], address) + if err != nil { + return errors.Wrapf(err, "unable to create socket %s", uri) + } + server, err := api.NewServerWithSettings(r, timeout, &l) + if err != nil { + return err + } + return server.Serve() +} + +func runVarlink(r *libpod.Runtime, uri string, timeout time.Duration, c *cliconfig.ServiceValues) error { + var varlinkInterfaces = []*iopodman.VarlinkInterface{varlinkapi.New(&c.PodmanCommand, r)} + service, err := varlink.NewService( + "Atomic", + "podman", + version.Version, + "https://github.com/containers/libpod", + ) + if err != nil { + return errors.Wrapf(err, "unable to create new varlink service") + } + + for _, i := range varlinkInterfaces { + if err := service.RegisterInterface(i); err != nil { + return errors.Errorf("unable to register varlink interface %v", i) + } + } + + // Run the varlink server at the given address + if err = service.Listen(uri, timeout); err != nil { + switch err.(type) { + case varlink.ServiceTimeoutError: + logrus.Infof("varlink service expired (use --timeout to increase session time beyond %d ms, 0 means never timeout)", timeout.String()) + return nil + default: + return errors.Wrapf(err, "unable to start varlink service") + } + } + return nil +} diff --git a/cmd/podman/service_dummy.go b/cmd/podman/service_dummy.go new file mode 100644 index 000000000..a774c34de --- /dev/null +++ b/cmd/podman/service_dummy.go @@ -0,0 +1,11 @@ +// +build !varlink + +package main + +import "github.com/spf13/cobra" + +var ( + _serviceCommand = &cobra.Command{ + Use: "", + } +) diff --git a/cmd/podman/varlink.go b/cmd/podman/varlink.go index cd21e3574..047d94fc2 100644 --- a/cmd/podman/varlink.go +++ b/cmd/podman/varlink.go @@ -51,7 +51,7 @@ func init() { } func varlinkCmd(c *cliconfig.VarlinkValues) error { - varlinkURI := adapter.DefaultAddress + varlinkURI := adapter.DefaultVarlinkAddress if rootless.IsRootless() { xdg, err := util.GetRuntimeDir() if err != nil { diff --git a/cmd/service/main.go b/cmd/service/main.go deleted file mode 100644 index 0290de892..000000000 --- a/cmd/service/main.go +++ /dev/null @@ -1,55 +0,0 @@ -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/commands.md b/commands.md index b744b702e..17e069cb1 100644 --- a/commands.md +++ b/commands.md @@ -73,6 +73,7 @@ | [podman-rmi(1)](/docs/source/markdown/podman-rmi.1.md) | Removes one or more images | | [podman-run(1)](/docs/source/markdown/podman-run.1.md) | Run a command in a container | | [podman-save(1)](/docs/source/markdown/podman-save.1.md) | Saves an image to an archive | +| [podman-service(1)](/docs/source/markdown/podman-service.1.md) | Run an API listening service | | [podman-search(1)](/docs/source/markdown/podman-search.1.md) | Search a registry for an image | | [podman-start(1)](/docs/source/markdown/podman-start.1.md) | Starts one or more containers | | [podman-stats(1)](/docs/source/markdown/podman-stats.1.md) | Display a live stream of one or more containers' resource usage statistics | diff --git a/completions/bash/podman b/completions/bash/podman index ca3618b0b..57b9547a7 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1755,6 +1755,19 @@ _podman_search() { _complete_ "$options_with_args" "$boolean_options" } +_podman_service() { + local options_with_args=" + -t + --timeout + " + local boolean_options=" + --help + -h + --varlink + " + _complete_ "$options_with_args" "$boolean_options" +} + _podman_unmount() { _podman_umount $@ } diff --git a/docs/source/markdown/podman-service.1.md b/docs/source/markdown/podman-service.1.md new file mode 100644 index 000000000..5c55e20d3 --- /dev/null +++ b/docs/source/markdown/podman-service.1.md @@ -0,0 +1,47 @@ +% podman-service(1) + +## NAME +podman\-service - Run an API service + +## SYNOPSIS +**podman service** [*options*] + +## DESCRIPTION +The **podman service** command creates a listening service that will answer API calls for Podman. You may +optionally provide an endpoint for the API in URI form. For example, *unix://tmp/foobar.sock* or *tcp:localhost:8080*. +If no endpoint is provided, defaults will be used. The default endpoint for a rootfull +service is *unix:/run/podman/podman.sock* and rootless is *unix:/$XDG_RUNTIME_DIR/podman/podman.sock* (for +example *unix:/run/user/1000/podman/podman.sock*) + +## OPTIONS + +**--timeout**, **-t** + +The time until the session expires in _milliseconds_. The default is 1 +second. A value of `0` means no timeout and the session will not expire. + +**--varlink** + +Use the varlink protocol instead of the REST-based protocol. This option will be deprecated in the future. + +**--help**, **-h** + +Print usage statement. + +## EXAMPLES + +Run an API listening for 5 seconds using the default socket. +``` +podman service --timeout 5000 +``` + +Run the podman varlink service with an alternate URI and accept the default timeout. +``` +$ podman service --varlink unix:/tmp/io.podman +``` + +## SEE ALSO +podman(1), podman-varlink(1) + +## HISTORY +January 2020, Originally compiled by Brent Baude<bbaude@redhat.com> diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 0c9ec3d1c..6e0eff045 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -191,6 +191,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-rmi(1)](podman-rmi.1.md) | Removes one or more locally stored images. | | [podman-run(1)](podman-run.1.md) | Run a command in a new container. | | [podman-save(1)](podman-save.1.md) | Save an image to a container archive. | +| [podman-service(1)](podman-service.1.md) | Run an API service | | [podman-search(1)](podman-search.1.md) | Search a registry for an image. | | [podman-start(1)](podman-start.1.md) | Start one or more containers. | | [podman-stats(1)](podman-stats.1.md) | Display a live stream of one or more container's resource usage statistics. | diff --git a/pkg/adapter/client.go b/pkg/adapter/client.go index da4670892..5774ebe72 100644 --- a/pkg/adapter/client.go +++ b/pkg/adapter/client.go @@ -57,7 +57,7 @@ func (r RemoteRuntime) RemoteEndpoint() (remoteEndpoint *Endpoint, err error) { // last resort is to make a socket connection with the default varlink address for root user } else { logrus.Debug("creating a varlink address based default root address") - remoteEndpoint, err = newSocketConnection(DefaultAddress) + remoteEndpoint, err = newSocketConnection(DefaultVarlinkAddress) } return } diff --git a/pkg/adapter/client_config.go b/pkg/adapter/client_config.go index 3559b16e3..8187b03b1 100644 --- a/pkg/adapter/client_config.go +++ b/pkg/adapter/client_config.go @@ -1,7 +1,10 @@ package adapter -// DefaultAddress is the default address of the varlink socket -const DefaultAddress = "unix:/run/podman/io.podman" +// DefaultAPIAddress is the default address of the REST socket +const DefaultAPIAddress = "unix:/run/podman/podman.sock" + +// DefaultVarlinkAddress is the default address of the varlink socket +const DefaultVarlinkAddress = "unix:/run/podman/io.podman" // EndpointType declares the type of server connection type EndpointType int diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go index d4cddbfb2..b4acdc312 100644 --- a/pkg/api/handlers/images.go +++ b/pkg/api/handlers/images.go @@ -74,8 +74,25 @@ func TagImage(w http.ResponseWriter, r *http.Request) { } func RemoveImage(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) + query := struct { + noPrune bool + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + muxVars := mux.Vars(r) + if _, found := muxVars["noprune"]; found { + if query.noPrune { + utils.UnSupportedParameter("noprune") + } + } name := mux.Vars(r)["name"] newImage, err := runtime.ImageRuntime().NewFromLocal(name) if err != nil { diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 388be24b6..e16a4ea1f 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -143,6 +143,22 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } +func UnmountContainer(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 + } + // TODO In future it might be an improvement that libpod unmount return a + // "container not mounted" error so we can surface that to the endpoint user + if err := conn.Unmount(false); err != nil { + utils.InternalServerError(w, err) + } + utils.WriteResponse(w, http.StatusNoContent, "") + +} func MountContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) name := mux.Vars(r)["name"] diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index daaf9d018..14f8e8de7 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -12,6 +12,7 @@ import ( "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/util" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -384,18 +385,27 @@ func PodKill(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) decoder = r.Context().Value("decoder").(*schema.Decoder) + signal = "SIGKILL" ) query := struct { - signal int `schema:"signal"` + signal string `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 } + muxVars := mux.Vars(r) + if _, found := muxVars["signal"]; found { + signal = query.signal + } + + sig, err := util.ParseSignal(signal) + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "unable to parse signal value")) + } name := mux.Vars(r)["name"] pod, err := runtime.LookupPod(name) if err != nil { @@ -419,8 +429,7 @@ func PodKill(w http.ResponseWriter, r *http.Request) { utils.Error(w, msg, http.StatusConflict, errors.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) return } - // TODO How do we differentiate if a signal was sent vs accepting the pod/container default? - _, err = pod.Kill(uint(query.signal)) + _, err = pod.Kill(uint(sig)) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index c845c8195..0db4e19b6 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -30,9 +30,7 @@ type swagImageInspect struct { // swagger:response DocsImageDeleteResponse type swagImageDeleteResponse struct { // in:body - Body struct { - image.ImageDeleteResponse - } + Body []image.ImageDeleteResponse } // Search results diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index 3ec0742bd..b6f125c58 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -86,3 +86,8 @@ func (e ErrorModel) Error() string { func (e ErrorModel) Cause() error { return errors.New(e.Because) } + +// UnsupportedParameter logs a given param by its string name as not supported. +func UnSupportedParameter(param string) { + log.Infof("API parameter %q: not supported", param) +} diff --git a/pkg/api/server/listener_api.go b/pkg/api/server/listener_api.go new file mode 100644 index 000000000..4984216b8 --- /dev/null +++ b/pkg/api/server/listener_api.go @@ -0,0 +1,31 @@ +package server + +import ( + "net" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +// ListenUnix follows stdlib net.Listen() API, providing a unix listener for given path +// ListenUnix will delete and create files/directories as needed +func ListenUnix(network string, path string) (net.Listener, error) { + // setup custom listener for API server + err := os.MkdirAll(filepath.Dir(path), 0770) + if err != nil { + return nil, errors.Wrapf(err, "api.ListenUnix() failed to create %s", filepath.Dir(path)) + } + os.Remove(path) + + listener, err := net.Listen(network, path) + if err != nil { + return nil, errors.Wrapf(err, "api.ListenUnix() failed to create net.Listen(%s, %s)", network, path) + } + + _, err = os.Stat(path) + if err != nil { + return nil, errors.Wrapf(err, "net.Listen(%s, %s) failed to report the failure to create socket", network, path) + } + return listener, nil +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 6f4222d8f..833bb5197 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -665,7 +665,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // '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 libpod mountContainer + // swagger:operation POST /libpod/containers/{nameOrID}/mount libpod mountContainer // --- // tags: // - containers @@ -684,12 +684,33 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // schema: // description: id // type: string - // example: 3c784de79b791b4ebd3ac55e511f97fedc042328499554937a3f8bfd9c1a2cb8 + // example: /var/lib/containers/storage/overlay/f3f693bd88872a1e3193f4ebb925f4c282e8e73aadb8ab3e7492754dda3a02a4/merged // '404': // "$ref": "#/responses/NoSuchContainer" // '500': // "$ref": "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/mount"), APIHandler(s.Context, libpod.MountContainer)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/{nameOrID}/unmount libpod unmountContainer + // --- + // tags: + // - containers + // summary: Unmount a container + // description: Unmount a container from the filesystem + // 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:..*}/unmount"), APIHandler(s.Context, libpod.UnmountContainer)).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 libpod libpodPauseContainer // --- diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index cd42afe71..7f1bb4e5c 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -193,8 +193,8 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // responses: // '200': // $ref: "#/responses/DocsImageDeleteResponse" - // '400': - // $ref: '#/responses/BadParamError' + // '404': + // $ref: '#/responses/NoSuchImage' // '409': // $ref: '#/responses/ConflictError' // '500': @@ -506,11 +506,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - application/json // responses: // '200': - // schema: - // items: - // $ref: "#/responses/DocsIageDeleteResponse" - // '400': - // $ref: "#/responses/BadParamError" + // $ref: "#/responses/DocsImageDeleteResponse" // '404': // $ref: '#/responses/NoSuchImage' // '409': @@ -533,10 +529,12 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // name: format // type: string // description: format for exported image + // default: oci-archive // - in: query // name: compress // type: bool // description: use compression on image + // default: false // produces: // - application/json // responses: diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 5069326b6..4018cfbe8 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -121,8 +121,9 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // description: the name or ID of the pod // - in: query // name: signal - // type: int + // type: string // description: signal to be sent to pod + // default: SIGKILL // responses: // '204': // description: no error diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 9abedb359..f3bae0345 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -54,9 +54,9 @@ import ( ) type APIServer struct { - http.Server // Where the HTTP work happens + http.Server // The HTTP work happens here *schema.Decoder // Decoder for Query parameters to structs - context.Context // Context for graceful server shutdown + context.Context // Context to carry objects to handlers *libpod.Runtime // Where the real work happens net.Listener // mux for routing HTTP API calls to libpod routines context.CancelFunc // Stop APIServer @@ -64,14 +64,37 @@ type APIServer struct { time.Duration // Duration of client access sliding window } -// NewServer will create and configure a new API HTTP server +// Number of seconds to wait for next request, if exceeded shutdown server +const ( + DefaultServiceDuration = 300 * time.Second + UnlimitedServiceDuration = 0 * time.Second +) + +// NewServer will create and configure a new API server with all defaults 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)) + return newServer(runtime, DefaultServiceDuration, nil) +} + +// NewServerWithSettings will create and configure a new API server using provided settings +func NewServerWithSettings(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) (*APIServer, error) { + return newServer(runtime, duration, listener) +} + +func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) (*APIServer, error) { + // If listener not provided try socket activation protocol + if listener == nil { + if _, found := os.LookupEnv("LISTEN_FDS"); !found { + return nil, errors.Errorf("Cannot create Server, no listener provided and socket activation protocol is not active.") + } + + 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 for socket activation protocol (%d != 1)", len(listeners)) + } + listener = &listeners[0] } router := mux.NewRouter() @@ -86,9 +109,9 @@ func NewServer(runtime *libpod.Runtime) (*APIServer, error) { Decoder: schema.NewDecoder(), Context: nil, Runtime: runtime, - Listener: listeners[0], + Listener: *listener, CancelFunc: nil, - Duration: 300 * time.Second, + Duration: duration, } server.Timer = time.AfterFunc(server.Duration, func() { if err := server.Shutdown(); err != nil { @@ -182,6 +205,11 @@ func (s *APIServer) Serve() error { // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { + // Duration == 0 flags no auto-shutdown of server + if s.Duration == 0 { + return nil + } + // We're still in the sliding service window if s.Timer.Stop() { s.Timer.Reset(s.Duration) diff --git a/pkg/bindings/containers.go b/pkg/bindings/containers.go index 01f68f970..057580088 100644 --- a/pkg/bindings/containers.go +++ b/pkg/bindings/containers.go @@ -126,11 +126,11 @@ func (c Connection) ContainerExists(nameOrID string) (bool, error) { 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? +func (c Connection) StopContainer(nameOrID string, timeout *int) error { params := make(map[string]string) - params["t"] = strconv.Itoa(timeout) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/stop", nameOrID), nil, params) if err != nil { return err |