From 6c45c7746612e7248908b424f2dac0fe5475aa75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 02:18:18 -0400 Subject: [PATCH 1/4] Add function in Portainer client to proxy a request to Docker API --- client/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/client.go b/client/client.go index 9d16569..66264de 100644 --- a/client/client.go +++ b/client/client.go @@ -66,6 +66,9 @@ type PortainerClient interface { // Run a function after receiving a response from Portainer AfterResponse(hook func(resp *http.Response) (err error)) + + // Proxy proxies a request to /endpoint/{id}/docker and returns its result + Proxy(endpointId portainer.EndpointID, req *http.Request) (resp *http.Response, err error) } type portainerClientImp struct { @@ -305,6 +308,11 @@ func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) { return } +func (n *portainerClientImp) Proxy(endpointId portainer.EndpointID, req *http.Request) (resp *http.Response, err error) { + resp, err = n.do(fmt.Sprintf("endpoints/%v/docker%s", endpointId, req.RequestURI), req.Method, req.Body, "", req.Header) + return +} + // Create a new client func NewClient(httpClient *http.Client, config Config) PortainerClient { return &portainerClientImp{ From 84374e6ba46fb2d08895d70c760c912e5550224a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 02:29:21 -0400 Subject: [PATCH 2/4] Add proxy command to start a proxy to an endpoint's Docker API --- cmd/proxy.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 cmd/proxy.go diff --git a/cmd/proxy.go b/cmd/proxy.go new file mode 100644 index 0000000..e7682d0 --- /dev/null +++ b/cmd/proxy.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "io/ioutil" + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/sirupsen/logrus" + + "github.com/spf13/viper" + + "github.com/greenled/portainer-stack-utils/common" + + "github.com/spf13/cobra" +) + +// proxyCmd represents the proxy command +var proxyCmd = &cobra.Command{ + Use: "proxy", + Short: "Start an HTTP proxy to an endpoint's Docker API", + Example: ` Expose "primary" endpoint's Docker API at 127.0.0.1:11000: + psu proxy --endpoint primary --address 127.0.0.1:11000 + + Configure local Docker client to connect to proxy: + export DOCKER_HOST=tcp://127.0.0.1:11000`, + Run: func(cmd *cobra.Command, args []string) { + portainerClient, err := common.GetClient() + common.CheckError(err) + + var endpoint portainer.Endpoint + if endpointName := viper.GetString("proxy.endpoint"); endpointName == "" { + // Guess endpoint if not set + logrus.WithFields(logrus.Fields{ + "implications": "Command will fail if there is not exactly one endpoint available", + }).Warning("Endpoint not set") + var endpointRetrievalErr error + endpoint, endpointRetrievalErr = common.GetDefaultEndpoint() + common.CheckError(endpointRetrievalErr) + logrus.WithFields(logrus.Fields{ + "endpoint": endpoint.Name, + }).Debug("Using the only available endpoint") + } else { + // Get endpoint by name + var endpointRetrievalErr error + endpoint, endpointRetrievalErr = common.GetEndpointByName(endpointName) + common.CheckError(endpointRetrievalErr) + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + resp, err := portainerClient.Proxy(endpoint.ID, r) + if err != nil { + logrus.Fatal(err) + } + + for key, value := range resp.Header { + for i := range value { + w.Header().Add(key, value[i]) + } + } + + w.WriteHeader(resp.StatusCode) + + if resp.Body != nil { + bodyBytes, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + logrus.Fatal(err) + } + w.Write(bodyBytes) + } + }) + + err = http.ListenAndServe(viper.GetString("proxy.address"), nil) + if err != http.ErrServerClosed { + logrus.Fatal(err) + } + }, +} + +func init() { + rootCmd.AddCommand(proxyCmd) + + proxyCmd.Flags().String("endpoint", "", "Endpoint name.") + proxyCmd.Flags().String("address", "127.0.0.1:2375", "Address to bind to.") + viper.BindPFlag("proxy.endpoint", proxyCmd.Flags().Lookup("endpoint")) + viper.BindPFlag("proxy.address", proxyCmd.Flags().Lookup("address")) +} From 0014a39265aee37b1b82a20153c2ad107d7465ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 02:44:41 -0400 Subject: [PATCH 3/4] Add Docker image envvars for proxy command --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7ed384f..32b0c75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ ENV PSU_AUTH_TOKEN="" \ PSU_LOG_LEVEL="" \ PSU_LOGIN_PRINT="" \ PSU_PASSWORD="" \ + PSU_PROXY_ADDRESS="" \ + PSU_PROXY_ENDPOINT="" \ PSU_STACK_DEPLOY_ENDPOINT="" \ PSU_STACK_DEPLOY_ENV_FILE="" \ PSU_STACK_DEPLOY_REPLACE_ENV="" \ From dca719a15eb44d686f47ac753e355c6b1d93425d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Mej=C3=ADas=20Rodr=C3=ADguez?= Date: Fri, 23 Aug 2019 03:01:28 -0400 Subject: [PATCH 4/4] Add endpoint's Docker API proxy section to Readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 1bdb3a5..d22f081 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [YAML configuration file](#yaml-configuration-file) - [JSON configuration file](#json-configuration-file) - [Environment variables for deployed stacks](#environment-variables-for-deployed-stacks) + - [Endpoint's Docker API proxy](#endpoints-docker-api-proxy) - [Log level](#log-level) - [Exit statuses](#exit-statuses) - [Contributing](#contributing) @@ -160,6 +161,26 @@ echo "stack.deploy.env-file: .env" > .config.yml psu stack deploy django-stack -c /path/to/docker-compose.yml --config .config.yml ``` +### Endpoint's Docker API proxy + +If you want finer-grained control over an endpoint's Docker daemon you can expose it through a proxy and configure a local Docker client to use it. + +First, expose the endpoint's Docker API: + +```bash +psu proxy --endpoint primary --address 127.0.0.1:2375 +``` + +Then (in a different shell), configure a local Docker client to use the exposed API: + +```bash +export DOCKER_HOST=tcp://127.0.0.1:2375 +``` + +Now you can run `docker ...` commands in the `primary` endpoint as in a local Docker installation, **with the added benefit of using Portainer's RBAC**. + +*Note that creating stacks through `docker stack ...` instead of `psu stack ...` will give you *limited* control over them, as they are created outside of Portainer.* + ### Log level You can control how much noise you want the program to do by setting the log level. There are seven log levels: