Merge pull request #30 from greenled/docker-proxy

Docker proxy
This commit is contained in:
Juan Carlos Mejías Rodríguez 2019-08-30 16:34:42 -04:00 committed by GitHub
commit 1ab06d66a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 0 deletions

View File

@ -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="" \

View File

@ -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:

View File

@ -60,6 +60,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 {
@ -121,6 +124,22 @@ func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, heade
return
}
func (n *portainerClientImp) doWithToken(uri, method string, requestBody io.Reader, headers http.Header) (resp *http.Response, err error) {
// Ensure there is an auth token
if n.token == "" {
n.token, err = n.AuthenticateUser(AuthenticateUserOptions{
Username: n.user,
Password: n.password,
})
if err != nil {
return
}
}
headers.Set("Authorization", "Bearer "+n.token)
return n.do(uri, method, requestBody, headers)
}
// Do a JSON http request
func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, requestBody interface{}, responseBody interface{}) error {
// Encode request body, if any

View File

@ -0,0 +1,12 @@
package client
import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
func (n *portainerClientImp) Proxy(endpointID portainer.EndpointID, req *http.Request) (resp *http.Response, err error) {
return n.doWithToken(fmt.Sprintf("endpoints/%v/docker%s", endpointID, req.RequestURI), req.Method, req.Body, req.Header)
}

87
cmd/proxy.go Normal file
View File

@ -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"))
}