mirror of
https://gitlab.com/psuapp/psu.git
synced 2024-08-30 18:12:34 +00:00
commit
1ab06d66a4
@ -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="" \
|
||||
|
21
README.md
21
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:
|
||||
|
@ -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
|
||||
|
12
client/endpoint_docker_proxy.go
Normal file
12
client/endpoint_docker_proxy.go
Normal 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
87
cmd/proxy.go
Normal 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"))
|
||||
}
|
Loading…
Reference in New Issue
Block a user