Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbae99ea87 | |||
| 3254051647 | |||
| f0d128f212 | |||
| a0b52fc3d7 | |||
| 31fdef1e60 | |||
| be30e1c453 | |||
| 5b55b890e7 | |||
| a5eac07b0c | |||
| fa80a7b7e5 | |||
| b14500a2d5 | |||
| 278667825a | |||
| 65ded647b6 | |||
| 084cdcd8dc | |||
| 5b68c4365e | |||
| 9cd64664cc | |||
| e831fa4a03 | |||
| 2a3c807978 | |||
| a8265a44d0 | |||
| 71ad21598b | |||
| 6e017ea64e | |||
| d48980e85b | |||
| 80d3fcc40b | |||
| 2e92706ead | |||
| d4fa9db432 | |||
| a28559777f | |||
| f6531627d4 | |||
| 535215833d | |||
| 666b09ad3b | |||
| c4a1243af9 | |||
| 305d0d2da0 | |||
| 9af9b70f3e | |||
| e4605d990d | |||
| 768697157c | |||
| d3086da139 | |||
| 95894e8047 | |||
| 81de55fedd | |||
| 84827b8782 | |||
| fa38af5d81 | |||
| 1b82b450d7 | |||
| b78d804881 | |||
| 51b72c12f9 | |||
| 58c04bdbe3 | |||
| a6320d5222 | |||
| cb4b4a43e6 | |||
| 1e5a1d5bdd | |||
| 5ed0d21c39 | |||
| 2972dbeafb |
@@ -4,7 +4,6 @@ about: Create a bug report
|
|||||||
title: ''
|
title: ''
|
||||||
labels: bug/need-confirmation, kind/bug
|
labels: bug/need-confirmation, kind/bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ about: Ask us a question about Portainer usage or deployment
|
|||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Before you start, we need a little bit more information from you:
|
Before you start, we need a little bit more information from you:
|
||||||
|
|
||||||
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
|
|||||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||||
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
|
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||||
@@ -402,7 +402,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||||
|
|
||||||
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||||
|
|
||||||
if dataStore.IsNew() {
|
if dataStore.IsNew() {
|
||||||
err = updateSettingsFromFlags(dataStore, flags)
|
err = updateSettingsFromFlags(dataStore, flags)
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -20,27 +23,64 @@ import (
|
|||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
||||||
type KubernetesDeployer struct {
|
type KubernetesDeployer struct {
|
||||||
binaryPath string
|
binaryPath string
|
||||||
dataStore portainer.DataStore
|
dataStore portainer.DataStore
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
kubernetesClientFactory *cli.ClientFactory
|
||||||
|
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||||
func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||||
return &KubernetesDeployer{
|
return &KubernetesDeployer{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
dataStore: datastore,
|
dataStore: datastore,
|
||||||
reverseTunnelService: reverseTunnelService,
|
reverseTunnelService: reverseTunnelService,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
|
kubernetesClientFactory: kubernetesClientFactory,
|
||||||
|
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||||
|
|
||||||
|
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return "", fmt.Errorf("can not get a valid user service account token")
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
||||||
// Otherwise it will use kubectl to deploy the manifest.
|
// Otherwise it will use kubectl to deploy the manifest.
|
||||||
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||||
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
token, err := deployer.getToken(request, endpoint, true);
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -53,7 +93,7 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo
|
|||||||
args := make([]string, 0)
|
args := make([]string, 0)
|
||||||
args = append(args, "--server", endpoint.URL)
|
args = append(args, "--server", endpoint.URL)
|
||||||
args = append(args, "--insecure-skip-tls-verify")
|
args = append(args, "--insecure-skip-tls-verify")
|
||||||
args = append(args, "--token", string(token))
|
args = append(args, "--token", token)
|
||||||
args = append(args, "--namespace", namespace)
|
args = append(args, "--namespace", namespace)
|
||||||
args = append(args, "apply", "-f", "-")
|
args = append(args, "apply", "-f", "-")
|
||||||
|
|
||||||
@@ -139,8 +179,14 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, err := deployer.getToken(request, endpoint, false);
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
||||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
|
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||||
|
|
||||||
resp, err := httpCli.Do(req)
|
resp, err := httpCli.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package gittypes
|
package gittypes
|
||||||
|
|
||||||
type RepoConfig struct {
|
type RepoConfig struct {
|
||||||
URL string
|
// The repo url
|
||||||
ReferenceName string
|
URL string `example:"https://github.com/portainer/portainer-ee.git"`
|
||||||
ConfigFilePath string
|
// The reference name
|
||||||
|
ReferenceName string `example:"refs/heads/branch_name"`
|
||||||
|
// Path to where the config file is in this url/refName
|
||||||
|
ConfigFilePath string `example:"docker-compose.yml"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
|
|||||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
|
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
|
||||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
@@ -153,6 +154,7 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
@@ -219,8 +221,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
|
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
|
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
|
||||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
@@ -366,9 +370,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
@@ -395,6 +401,7 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc
|
|||||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
|
||||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
|
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
|
||||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
@@ -134,14 +133,6 @@ func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User)
|
|||||||
return handler.persistAndWriteToken(w, composeTokenData(user))
|
return handler.persistAndWriteToken(w, composeTokenData(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
|
|
||||||
token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
|
|
||||||
}
|
|
||||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
||||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
@@ -26,21 +25,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) {
|
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
return "", nil, errors.New("Invalid OAuth authorization code")
|
return "", errors.New("Invalid OAuth authorization code")
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
return "", nil, errors.New("Invalid OAuth configuration")
|
return "", errors.New("Invalid OAuth configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
username, expiryTime, err := handler.OAuthService.Authenticate(code, settings)
|
username, err := handler.OAuthService.Authenticate(code, settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return username, expiryTime, nil
|
return username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id ValidateOAuth
|
// @id ValidateOAuth
|
||||||
@@ -70,7 +69,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
|
||||||
}
|
}
|
||||||
|
|
||||||
username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
|
||||||
@@ -111,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.writeTokenForOAuth(w, user, expiryTime)
|
return handler.writeToken(w, user)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,8 +115,18 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
|||||||
return nil, errors.New("failed fetching dockerhub limits")
|
return nil, errors.New("failed fetching dockerhub limits")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An error with rateLimit-Limit or RateLimit-Remaining is likely for dockerhub pro accounts where there is no rate limit.
|
||||||
|
// In that specific case the headers will not be present. Don't bubble up the error as its normal
|
||||||
|
// See: https://docs.docker.com/docker-hub/download-rate-limit/
|
||||||
rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit")
|
rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining")
|
rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
return &dockerhubStatusResponse{
|
return &dockerhubStatusResponse{
|
||||||
Limit: rateLimit,
|
Limit: rateLimit,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @title PortainerCE API
|
// @title PortainerCE API
|
||||||
// @version 2.1.1
|
// @version 2.6.2
|
||||||
// @description.markdown api-description.md
|
// @description.markdown api-description.md
|
||||||
// @termsOfService
|
// @termsOfService
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
|||||||
doCleanUp := true
|
doCleanUp := true
|
||||||
defer handler.cleanUp(stack, &doCleanUp)
|
defer handler.cleanUp(stack, &doCleanUp)
|
||||||
|
|
||||||
output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
|
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
|||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
|
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
|||||||
return response.JSON(w, resp)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
|
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
|
||||||
handler.stackCreationMutex.Lock()
|
handler.stackCreationMutex.Lock()
|
||||||
defer handler.stackCreationMutex.Unlock()
|
defer handler.stackCreationMutex.Unlock()
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stac
|
|||||||
stackConfig = string(convertedConfig)
|
stackConfig = string(convertedConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace)
|
return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
// @success 204 "Success"
|
// @success 204 "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request"
|
||||||
// @failure 403 "Permission denied"
|
// @failure 403 "Permission denied"
|
||||||
// @failure 404 " not found"
|
// @failure 404 "Not found"
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /stacks/{id} [delete]
|
// @router /stacks/{id} [delete]
|
||||||
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
// @success 200 {object} portainer.Stack "Success"
|
// @success 200 {object} portainer.Stack "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request"
|
||||||
// @failure 403 "Permission denied"
|
// @failure 403 "Permission denied"
|
||||||
// @failure 404 " not found"
|
// @failure 404 "Not found"
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /stacks/{id}/start [post]
|
// @router /stacks/{id}/start [post]
|
||||||
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
// @success 200 {object} portainer.Stack "Success"
|
// @success 200 {object} portainer.Stack "Success"
|
||||||
// @failure 400 "Invalid request"
|
// @failure 400 "Invalid request"
|
||||||
// @failure 403 "Permission denied"
|
// @failure 403 "Permission denied"
|
||||||
// @failure 404 " not found"
|
// @failure 404 "Not found"
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /stacks/{id}/stop [post]
|
// @router /stacks/{id}/stop [post]
|
||||||
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
|||||||
@@ -33,7 +33,23 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
|
// @id StackUpdateGit
|
||||||
|
// @summary Redeploy a stack
|
||||||
|
// @description Pull and redeploy a stack via Git
|
||||||
|
// @description **Access policy**: restricted
|
||||||
|
// @tags stacks
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param id path int true "Stack identifier"
|
||||||
|
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||||
|
// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack"
|
||||||
|
// @success 200 {object} portainer.Stack "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /stacks/{id}/git [put]
|
||||||
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
)
|
)
|
||||||
@@ -12,20 +13,22 @@ import (
|
|||||||
// Handler is the HTTP handler used to handle websocket operations.
|
// Handler is the HTTP handler used to handle websocket operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
KubernetesClientFactory *cli.ClientFactory
|
KubernetesClientFactory *cli.ClientFactory
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
connectionUpgrader websocket.Upgrader
|
connectionUpgrader websocket.Upgrader
|
||||||
|
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage websocket operations.
|
// NewHandler creates a handler to manage websocket operations.
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
connectionUpgrader: websocket.Upgrader{},
|
connectionUpgrader: websocket.Upgrader{},
|
||||||
requestBouncer: bouncer,
|
requestBouncer: bouncer,
|
||||||
|
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||||
}
|
}
|
||||||
h.PathPrefix("/websocket/exec").Handler(
|
h.PathPrefix("/websocket/exec").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +13,7 @@ import (
|
|||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @summary Execute a websocket on pod
|
// @summary Execute a websocket on pod
|
||||||
@@ -70,8 +73,14 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
|||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, useAdminToken, err := handler.getToken(r, endpoint, false)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err}
|
||||||
|
}
|
||||||
|
|
||||||
params := &webSocketRequestParams{
|
params := &webSocketRequestParams{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
|
token: token,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Header.Del("Origin")
|
r.Header.Del("Origin")
|
||||||
@@ -112,7 +121,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
|||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
err = cli.StartExecProcess(token, useAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||||
}
|
}
|
||||||
@@ -124,3 +133,37 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCache := handler.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||||
|
|
||||||
|
tokenManager, err := kubernetes.NewTokenManager(kubecli, handler.DataStore, tokenCache, setLocalAdminToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return tokenManager.GetAdminServiceAccountToken(), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return "", false, fmt.Errorf("can not get a valid user service account token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
|
|||||||
|
|
||||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||||
|
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||||
@@ -64,6 +65,7 @@ func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *htt
|
|||||||
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
||||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||||
|
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.ServeHTTP(w, r)
|
proxy.ServeHTTP(w, r)
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ type webSocketRequestParams struct {
|
|||||||
ID string
|
ID string
|
||||||
nodeName string
|
nodeName string
|
||||||
endpoint *portainer.Endpoint
|
endpoint *portainer.Endpoint
|
||||||
|
token string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
transport, err := kubernetes.NewLocalTransport(tokenManager)
|
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
|
|||||||
|
|
||||||
endpointURL.Scheme = "http"
|
endpointURL.Scheme = "http"
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||||
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, tokenManager)
|
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint, tokenManager)
|
||||||
|
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||||
proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager)
|
proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager, endpoint)
|
||||||
|
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agentTransport struct {
|
||||||
|
*baseTransport
|
||||||
|
signatureService portainer.DigitalSignatureService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||||
|
func NewAgentTransport(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint) *agentTransport {
|
||||||
|
transport := &agentTransport{
|
||||||
|
signatureService: signatureService,
|
||||||
|
baseTransport: newBaseTransport(
|
||||||
|
&http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
},
|
||||||
|
tokenManager,
|
||||||
|
endpoint,
|
||||||
|
dataStore,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||||
|
|
||||||
|
if strings.HasPrefix(request.URL.Path, "/v2") {
|
||||||
|
decorateAgentRequest(request, transport.dataStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||||
|
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
|
|
||||||
|
return transport.baseTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type edgeTransport struct {
|
||||||
|
*baseTransport
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
|
||||||
|
func NewEdgeTransport(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager) *edgeTransport {
|
||||||
|
transport := &edgeTransport{
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
|
baseTransport: newBaseTransport(
|
||||||
|
&http.Transport{},
|
||||||
|
tokenManager,
|
||||||
|
endpoint,
|
||||||
|
dataStore,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||||
|
|
||||||
|
if strings.HasPrefix(request.URL.Path, "/v2") {
|
||||||
|
decorateAgentRequest(request, transport.dataStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := transport.baseTransport.RoundTrip(request)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
|
||||||
|
} else {
|
||||||
|
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localTransport struct {
|
||||||
|
*baseTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
|
||||||
|
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) (*localTransport, error) {
|
||||||
|
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &localTransport{
|
||||||
|
baseTransport: newBaseTransport(
|
||||||
|
&http.Transport{
|
||||||
|
TLSClientConfig: config,
|
||||||
|
},
|
||||||
|
tokenManager,
|
||||||
|
endpoint,
|
||||||
|
dataStore,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
|
||||||
|
return transport.baseTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (transport *baseTransport) deleteNamespaceRequest(request *http.Request, namespace string) (*http.Response, error) {
|
||||||
|
if err := transport.tokenManager.kubecli.NamespaceAccessPoliciesDeleteNamespace(namespace); err != nil {
|
||||||
|
return nil, errors.WithMessagef(err, "failed to delete a namespace [%s] from portainer config", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeKubernetesRequest(request, true)
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
@@ -13,7 +11,6 @@ type tokenManager struct {
|
|||||||
tokenCache *tokenCache
|
tokenCache *tokenCache
|
||||||
kubecli portainer.KubeClient
|
kubecli portainer.KubeClient
|
||||||
dataStore portainer.DataStore
|
dataStore portainer.DataStore
|
||||||
mutex sync.Mutex
|
|
||||||
adminToken string
|
adminToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +22,6 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
|
|||||||
tokenCache: cache,
|
tokenCache: cache,
|
||||||
kubecli: kubecli,
|
kubecli: kubecli,
|
||||||
dataStore: dataStore,
|
dataStore: dataStore,
|
||||||
mutex: sync.Mutex{},
|
|
||||||
adminToken: "",
|
adminToken: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +37,13 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
|
|||||||
return tokenManager, nil
|
return tokenManager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *tokenManager) getAdminServiceAccountToken() string {
|
func (manager *tokenManager) GetAdminServiceAccountToken() string {
|
||||||
return manager.adminToken
|
return manager.adminToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
|
func (manager *tokenManager) GetUserServiceAccountToken(userID int) (string, error) {
|
||||||
manager.mutex.Lock()
|
manager.tokenCache.mutex.Lock()
|
||||||
defer manager.mutex.Unlock()
|
defer manager.tokenCache.mutex.Unlock()
|
||||||
|
|
||||||
token, ok := manager.tokenCache.getToken(userID)
|
token, ok := manager.tokenCache.getToken(userID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kubernetes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,7 @@ type (
|
|||||||
|
|
||||||
tokenCache struct {
|
tokenCache struct {
|
||||||
userTokenCache cmap.ConcurrentMap
|
userTokenCache cmap.ConcurrentMap
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +37,18 @@ func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *tokenCache {
|
|||||||
return tokenCache
|
return tokenCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOrCreateTokenCache will get the tokenCache from the manager map of caches if it exists,
|
||||||
|
// otherwise it will create a new tokenCache object, associate it to the manager map of caches
|
||||||
|
// and return a pointer to that tokenCache instance.
|
||||||
|
func (manager *TokenCacheManager) GetOrCreateTokenCache(endpointID int) *tokenCache {
|
||||||
|
key := strconv.Itoa(endpointID)
|
||||||
|
if epCache, ok := manager.tokenCaches.Get(key); ok {
|
||||||
|
return epCache.(*tokenCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.CreateTokenCache(endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveUserFromCache will ensure that the specific userID is removed from all registered caches.
|
// RemoveUserFromCache will ensure that the specific userID is removed from all registered caches.
|
||||||
func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
|
func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
|
||||||
for cache := range manager.tokenCaches.IterBuffered() {
|
for cache := range manager.tokenCaches.IterBuffered() {
|
||||||
@@ -45,6 +59,7 @@ func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
|
|||||||
func newTokenCache() *tokenCache {
|
func newTokenCache() *tokenCache {
|
||||||
return &tokenCache{
|
return &tokenCache{
|
||||||
userTokenCache: cmap.New(),
|
userTokenCache: cmap.New(),
|
||||||
|
mutex: sync.Mutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,147 +2,73 @@ package kubernetes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type baseTransport struct {
|
||||||
localTransport struct {
|
httpTransport *http.Transport
|
||||||
httpTransport *http.Transport
|
tokenManager *tokenManager
|
||||||
tokenManager *tokenManager
|
endpoint *portainer.Endpoint
|
||||||
endpointIdentifier portainer.EndpointID
|
dataStore portainer.DataStore
|
||||||
}
|
|
||||||
|
|
||||||
agentTransport struct {
|
|
||||||
dataStore portainer.DataStore
|
|
||||||
httpTransport *http.Transport
|
|
||||||
tokenManager *tokenManager
|
|
||||||
signatureService portainer.DigitalSignatureService
|
|
||||||
endpointIdentifier portainer.EndpointID
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeTransport struct {
|
|
||||||
dataStore portainer.DataStore
|
|
||||||
httpTransport *http.Transport
|
|
||||||
tokenManager *tokenManager
|
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
|
||||||
endpointIdentifier portainer.EndpointID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
|
|
||||||
func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
|
|
||||||
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := &localTransport{
|
|
||||||
httpTransport: &http.Transport{
|
|
||||||
TLSClientConfig: config,
|
|
||||||
},
|
|
||||||
tokenManager: tokenManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
return transport, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) *baseTransport {
|
||||||
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
return &baseTransport{
|
||||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
httpTransport: httpTransport,
|
||||||
if err != nil {
|
tokenManager: tokenManager,
|
||||||
return nil, err
|
endpoint: endpoint,
|
||||||
|
dataStore: dataStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/api/v[0-9](\.[0-9])?`)
|
||||||
|
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.EqualFold(requestPath, "/namespaces"):
|
||||||
|
return transport.executeKubernetesRequest(request, true)
|
||||||
|
case strings.HasPrefix(requestPath, "/namespaces"):
|
||||||
|
return transport.proxyNamespacedRequest(request, requestPath)
|
||||||
|
default:
|
||||||
|
return transport.executeKubernetesRequest(request, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fullRequestPath string) (*http.Response, error) {
|
||||||
|
requestPath := strings.TrimPrefix(fullRequestPath, "/namespaces/")
|
||||||
|
split := strings.SplitN(requestPath, "/", 2)
|
||||||
|
namespace := split[0]
|
||||||
|
|
||||||
|
requestPath = ""
|
||||||
|
if len(split) > 1 {
|
||||||
|
requestPath = split[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
switch {
|
||||||
|
case requestPath == "" && request.Method == "DELETE":
|
||||||
|
return transport.deleteNamespaceRequest(request, namespace)
|
||||||
|
default:
|
||||||
|
return transport.executeKubernetesRequest(request, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *baseTransport) executeKubernetesRequest(request *http.Request, shouldLog bool) (*http.Response, error) {
|
||||||
return transport.httpTransport.RoundTrip(request)
|
return transport.httpTransport.RoundTrip(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
var (
|
||||||
func NewAgentTransport(datastore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport {
|
namespaceRegex = regexp.MustCompile(`^/namespaces/([^/]*)$`)
|
||||||
transport := &agentTransport{
|
)
|
||||||
dataStore: datastore,
|
|
||||||
httpTransport: &http.Transport{
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
},
|
|
||||||
tokenManager: tokenManager,
|
|
||||||
signatureService: signatureService,
|
|
||||||
}
|
|
||||||
|
|
||||||
return transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
|
||||||
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
|
||||||
|
|
||||||
if strings.HasPrefix(request.URL.Path, "/v2") {
|
|
||||||
decorateAgentRequest(request, transport.dataStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
|
||||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
|
||||||
|
|
||||||
return transport.httpTransport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEdgeTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
|
|
||||||
func NewEdgeTransport(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport {
|
|
||||||
transport := &edgeTransport{
|
|
||||||
dataStore: datastore,
|
|
||||||
httpTransport: &http.Transport{},
|
|
||||||
tokenManager: tokenManager,
|
|
||||||
reverseTunnelService: reverseTunnelService,
|
|
||||||
endpointIdentifier: endpointIdentifier,
|
|
||||||
}
|
|
||||||
|
|
||||||
return transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
|
||||||
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
|
||||||
|
|
||||||
if strings.HasPrefix(request.URL.Path, "/v2") {
|
|
||||||
decorateAgentRequest(request, transport.dataStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := transport.httpTransport.RoundTrip(request)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier)
|
|
||||||
} else {
|
|
||||||
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRoundTripToken(
|
func getRoundTripToken(
|
||||||
request *http.Request,
|
request *http.Request,
|
||||||
@@ -156,9 +82,9 @@ func getRoundTripToken(
|
|||||||
|
|
||||||
var token string
|
var token string
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
token = tokenManager.getAdminServiceAccountToken()
|
token = tokenManager.GetAdminServiceAccountToken()
|
||||||
} else {
|
} else {
|
||||||
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed retrieving service account token: %v", err)
|
log.Printf("Failed retrieving service account token: %v", err)
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
+1
-1
@@ -204,7 +204,7 @@ func (server *Server) Start() error {
|
|||||||
userHandler.DataStore = server.DataStore
|
userHandler.DataStore = server.DataStore
|
||||||
userHandler.CryptoService = server.CryptoService
|
userHandler.CryptoService = server.CryptoService
|
||||||
|
|
||||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer)
|
||||||
websocketHandler.DataStore = server.DataStore
|
websocketHandler.DataStore = server.DataStore
|
||||||
websocketHandler.SignatureService = server.SignatureService
|
websocketHandler.SignatureService = server.SignatureService
|
||||||
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -80,6 +81,21 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||||
|
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||||
|
kcl.lock.Lock()
|
||||||
|
defer kcl.lock.Unlock()
|
||||||
|
|
||||||
|
policies, err := kcl.GetNamespaceAccessPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed to fetch access policies")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(policies, ns)
|
||||||
|
|
||||||
|
return kcl.UpdateNamespaceAccessPolicies(policies)
|
||||||
|
}
|
||||||
|
|
||||||
// GetNamespaceAccessPolicies gets the namespace access policies
|
// GetNamespaceAccessPolicies gets the namespace access policies
|
||||||
// from config maps in the portainer namespace
|
// from config maps in the portainer namespace
|
||||||
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
|
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
ktypes "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
kfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConfigExists(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
namespaceToDelete string
|
||||||
|
expectedConfig map[string]portainer.K8sNamespaceAccessPolicy
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "doesn't change config, when designated namespace absent",
|
||||||
|
namespaceToDelete: "missing-namespace",
|
||||||
|
expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{
|
||||||
|
"ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||||
|
"ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removes designated namespace from config, when namespace is present",
|
||||||
|
namespaceToDelete: "ns2",
|
||||||
|
expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{
|
||||||
|
"ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testcases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
k := &KubeClient{
|
||||||
|
cli: kfake.NewSimpleClientset(),
|
||||||
|
instanceID: "instance",
|
||||||
|
lock: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ktypes.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: portainerConfigMapName,
|
||||||
|
Namespace: portainerNamespace,
|
||||||
|
},
|
||||||
|
Data: map[string]string{
|
||||||
|
"NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := k.cli.CoreV1().ConfigMaps(portainerNamespace).Create(config)
|
||||||
|
assert.NoError(t, err, "failed to create a portainer config")
|
||||||
|
defer func() {
|
||||||
|
k.cli.CoreV1().ConfigMaps(portainerNamespace).Delete(portainerConfigMapName, nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = k.NamespaceAccessPoliciesDeleteNamespace(test.namespaceToDelete)
|
||||||
|
assert.NoError(t, err, "failed to delete namespace")
|
||||||
|
|
||||||
|
policies, err := k.GetNamespaceAccessPolicies()
|
||||||
|
assert.NoError(t, err, "failed to fetch policies")
|
||||||
|
assert.Equal(t, test.expectedConfig, policies)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
cmap "github.com/orcaman/concurrent-map"
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
|
||||||
@@ -25,8 +26,9 @@ type (
|
|||||||
|
|
||||||
// KubeClient represent a service used to execute Kubernetes operations
|
// KubeClient represent a service used to execute Kubernetes operations
|
||||||
KubeClient struct {
|
KubeClient struct {
|
||||||
cli *kubernetes.Clientset
|
cli kubernetes.Interface
|
||||||
instanceID string
|
instanceID string
|
||||||
|
lock *sync.Mutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (po
|
|||||||
kubecli := &KubeClient{
|
kubecli := &KubeClient{
|
||||||
cli: cli,
|
cli: cli,
|
||||||
instanceID: factory.instanceID,
|
instanceID: factory.instanceID,
|
||||||
|
lock: &sync.Mutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return kubecli, nil
|
return kubecli, nil
|
||||||
|
|||||||
@@ -14,13 +14,18 @@ import (
|
|||||||
// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace
|
// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace
|
||||||
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
||||||
// to the stdout parameter.
|
// to the stdout parameter.
|
||||||
// This function only works against a local endpoint using an in-cluster config.
|
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
|
||||||
func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
||||||
config, err := rest.InClusterConfig()
|
config, err := rest.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !useAdminToken {
|
||||||
|
config.BearerToken = token
|
||||||
|
config.BearerTokenFile = ""
|
||||||
|
}
|
||||||
|
|
||||||
req := kcl.cli.CoreV1().RESTClient().
|
req := kcl.cli.CoreV1().RESTClient().
|
||||||
Post().
|
Post().
|
||||||
Resource("pods").
|
Resource("pods").
|
||||||
|
|||||||
+4
-5
@@ -9,7 +9,6 @@ import (
|
|||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
@@ -27,18 +26,18 @@ func NewService() *Service {
|
|||||||
// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint.
|
// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint.
|
||||||
// On success, it will then return the username and token expiry time associated to authenticated user by fetching this information
|
// On success, it will then return the username and token expiry time associated to authenticated user by fetching this information
|
||||||
// from the resource server and matching it with the user identifier setting.
|
// from the resource server and matching it with the user identifier setting.
|
||||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, *time.Time, error) {
|
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||||
token, err := getOAuthToken(code, configuration)
|
token, err := getOAuthToken(code, configuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
|
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
|
||||||
return "", nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
username, err := getUsername(token.AccessToken, configuration)
|
username, err := getUsername(token.AccessToken, configuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
|
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
|
||||||
return "", nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
return username, &token.Expiry, nil
|
return username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
||||||
|
|||||||
+6
-4
@@ -2,6 +2,7 @@ package portainer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
@@ -1164,14 +1165,15 @@ type (
|
|||||||
KubeClient interface {
|
KubeClient interface {
|
||||||
SetupUserServiceAccount(userID int, teamIDs []int) error
|
SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||||
|
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||||
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
||||||
KubernetesDeployer interface {
|
KubernetesDeployer interface {
|
||||||
Deploy(endpoint *Endpoint, data string, namespace string) (string, error)
|
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
|
||||||
ConvertCompose(data string) ([]byte, error)
|
ConvertCompose(data string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1189,7 +1191,7 @@ type (
|
|||||||
|
|
||||||
// OAuthService represents a service used to authenticate users using OAuth
|
// OAuthService represents a service used to authenticate users using OAuth
|
||||||
OAuthService interface {
|
OAuthService interface {
|
||||||
Authenticate(code string, configuration *OAuthSettings) (string, *time.Time, error)
|
Authenticate(code string, configuration *OAuthSettings) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryService represents a service for managing registry data
|
// RegistryService represents a service for managing registry data
|
||||||
@@ -1341,7 +1343,7 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "2.6.0"
|
APIVersion = "2.6.2"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 30
|
DBVersion = 30
|
||||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default class porImageRegistryContainerController {
|
|||||||
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
|
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
|
||||||
try {
|
try {
|
||||||
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
|
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
|
||||||
this.setValidity(this.pullRateLimits.remaining >= 0);
|
this.setValidity(!this.pullRateLimits.limit || (this.pullRateLimits.limit && this.pullRateLimits.remaining >= 0));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('Failed loading DockerHub pull rate limits', e);
|
console.error('Failed loading DockerHub pull rate limits', e);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.downloadLogs = function () {
|
this.downloadLogs = function () {
|
||||||
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
|
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log.line, '')]);
|
||||||
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
|
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<div style="margin: 15px;">
|
<div style="margin: 15px;">
|
||||||
<span class="btn btn-primary btn-sm" ng-click="$ctrl.copyYAML()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy to clipboard</span>
|
<span class="btn btn-primary btn-sm" ng-click="$ctrl.copyYAML()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy to clipboard</span>
|
||||||
<span class="btn btn-primary btn-sm space-left" ng-click="$ctrl.toggleYAMLInspectorExpansion()">
|
<span class="btn btn-primary btn-sm space-left" ng-click="$ctrl.toggleYAMLInspectorExpansion()">
|
||||||
<i class="fa fa-{{ $ctrl.expanded ? 'minus' : 'plus' }} space-right" aria-hidden="true"></i>{{ $ctrl.expanded ? 'Collapse' : 'Expand' }}</span>
|
<i class="fa fa-{{ $ctrl.expanded ? 'minus' : 'plus' }} space-right" aria-hidden="true"></i>{{ $ctrl.expanded ? 'Collapse' : 'Expand' }}</span
|
||||||
|
>
|
||||||
<span id="copyNotificationYAML" style="margin-left: 7px; display: none; color: #23ae89;" class="small"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
<span id="copyNotificationYAML" style="margin-left: 7px; display: none; color: #23ae89;" class="small"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class KubernetesApplicationConverter {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) {
|
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels, ingresses) {
|
||||||
const res = new KubernetesApplicationFormValues();
|
const res = new KubernetesApplicationFormValues();
|
||||||
res.ApplicationType = app.ApplicationType;
|
res.ApplicationType = app.ApplicationType;
|
||||||
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
|
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
|
||||||
@@ -278,7 +278,7 @@ class KubernetesApplicationConverter {
|
|||||||
res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
|
res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
|
||||||
res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
|
res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
|
||||||
res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount);
|
res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount);
|
||||||
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts);
|
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts, ingresses);
|
||||||
res.Containers = app.Containers;
|
res.Containers = app.Containers;
|
||||||
|
|
||||||
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;
|
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ class KubernetesApplicationHelper {
|
|||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
|
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
|
||||||
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) {
|
||||||
const generatePort = (port, rule) => {
|
const generatePort = (port, rule) => {
|
||||||
const res = new KubernetesApplicationPublishedPortFormValue();
|
const res = new KubernetesApplicationPublishedPortFormValue();
|
||||||
res.IsNew = false;
|
res.IsNew = false;
|
||||||
@@ -282,6 +282,7 @@ class KubernetesApplicationHelper {
|
|||||||
res.IngressName = rule.IngressName;
|
res.IngressName = rule.IngressName;
|
||||||
res.IngressRoute = rule.Path;
|
res.IngressRoute = rule.Path;
|
||||||
res.IngressHost = rule.Host;
|
res.IngressHost = rule.Host;
|
||||||
|
res.IngressHosts = ingress && ingress.find((i) => i.Name === rule.IngressName).Hosts;
|
||||||
}
|
}
|
||||||
res.Protocol = port.Protocol;
|
res.Protocol = port.Protocol;
|
||||||
res.ContainerPort = port.TargetPort;
|
res.ContainerPort = port.TargetPort;
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export class KubernetesIngressConverter {
|
|||||||
rule.IngressName = ingress.Name;
|
rule.IngressName = ingress.Name;
|
||||||
rule.ServiceName = serviceName;
|
rule.ServiceName = serviceName;
|
||||||
rule.Port = p.ContainerPort;
|
rule.Port = p.ContainerPort;
|
||||||
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
|
if (p.IngressRoute) {
|
||||||
|
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
|
||||||
|
}
|
||||||
rule.Host = p.IngressHost;
|
rule.Host = p.IngressHost;
|
||||||
ingress.Paths.push(rule);
|
ingress.Paths.push(rule);
|
||||||
}
|
}
|
||||||
@@ -171,7 +173,7 @@ export class KubernetesIngressConverter {
|
|||||||
res.spec.rules = [];
|
res.spec.rules = [];
|
||||||
_.forEach(data.Hosts, (host) => {
|
_.forEach(data.Hosts, (host) => {
|
||||||
if (!host.NeedsDeletion) {
|
if (!host.NeedsDeletion) {
|
||||||
res.spec.rules.push({ host: host.Host });
|
res.spec.rules.push({ host: host.Host || host });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export function KubernetesApplicationPublishedPortFormValue() {
|
|||||||
IngressName: undefined,
|
IngressName: undefined,
|
||||||
IngressRoute: undefined,
|
IngressRoute: undefined,
|
||||||
IngressHost: undefined,
|
IngressHost: undefined,
|
||||||
|
IngressHosts: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1368,7 +1368,7 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
name="ingress_hostname_{{ $index }}"
|
name="ingress_hostname_{{ $index }}"
|
||||||
ng-model="publishedPort.IngressHost"
|
ng-model="publishedPort.IngressHost"
|
||||||
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in ctrl.ingressHostnames"
|
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in publishedPort.IngressHosts"
|
||||||
ng-change="ctrl.onChangePublishedPorts()"
|
ng-change="ctrl.onChangePublishedPorts()"
|
||||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ class KubernetesCreateApplicationController {
|
|||||||
const ingresses = this.filteredIngresses;
|
const ingresses = this.filteredIngresses;
|
||||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||||
|
p.IngressHosts = ingresses && ingresses.length ? ingresses[0].Hosts : undefined;
|
||||||
if (this.formValues.PublishedPorts.length) {
|
if (this.formValues.PublishedPorts.length) {
|
||||||
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
||||||
}
|
}
|
||||||
@@ -388,6 +389,7 @@ class KubernetesCreateApplicationController {
|
|||||||
onChangePortMappingIngress(index) {
|
onChangePortMappingIngress(index) {
|
||||||
const publishedPort = this.formValues.PublishedPorts[index];
|
const publishedPort = this.formValues.PublishedPorts[index];
|
||||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||||
|
publishedPort.IngressHosts = ingress.Hosts;
|
||||||
this.ingressHostnames = ingress.Hosts;
|
this.ingressHostnames = ingress.Hosts;
|
||||||
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
|
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
|
||||||
this.onChangePublishedPorts();
|
this.onChangePublishedPorts();
|
||||||
@@ -972,7 +974,8 @@ class KubernetesCreateApplicationController {
|
|||||||
this.resourcePools,
|
this.resourcePools,
|
||||||
this.configurations,
|
this.configurations,
|
||||||
this.persistentVolumeClaims,
|
this.persistentVolumeClaims,
|
||||||
this.nodesLabels
|
this.nodesLabels,
|
||||||
|
this.filteredIngresses
|
||||||
);
|
);
|
||||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class KubernetesCreateResourcePoolController {
|
|||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.defaults = KubernetesResourceQuotaDefaults;
|
this.defaults = KubernetesResourceQuotaDefaults;
|
||||||
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
|
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
|
||||||
|
this.formValues.HasQuota = true;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
|||||||
-8
@@ -1,12 +1,4 @@
|
|||||||
import { KEY_REGEX, VALUE_REGEX } from '@/portainer/helpers/env-vars';
|
|
||||||
|
|
||||||
class EnvironmentVariablesSimpleModeItemController {
|
class EnvironmentVariablesSimpleModeItemController {
|
||||||
/* @ngInject */
|
|
||||||
constructor() {
|
|
||||||
this.KEY_REGEX = KEY_REGEX;
|
|
||||||
this.VALUE_REGEX = VALUE_REGEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeName(name) {
|
onChangeName(name) {
|
||||||
const fieldIsInvalid = typeof name === 'undefined';
|
const fieldIsInvalid = typeof name === 'undefined';
|
||||||
if (fieldIsInvalid) {
|
if (fieldIsInvalid) {
|
||||||
|
|||||||
-2
@@ -9,7 +9,6 @@
|
|||||||
placeholder="e.g. FOO"
|
placeholder="e.g. FOO"
|
||||||
ng-model="$ctrl.variable.name"
|
ng-model="$ctrl.variable.name"
|
||||||
ng-disabled="$ctrl.variable.added"
|
ng-disabled="$ctrl.variable.added"
|
||||||
ng-pattern="$ctrl.KEY_REGEX"
|
|
||||||
ng-change="$ctrl.onChangeName($ctrl.variable.name)"
|
ng-change="$ctrl.onChangeName($ctrl.variable.name)"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
ng-model="$ctrl.variable.value"
|
ng-model="$ctrl.variable.value"
|
||||||
placeholder="e.g. bar"
|
placeholder="e.g. bar"
|
||||||
ng-trim="false"
|
ng-trim="false"
|
||||||
ng-pattern="$ctrl.VALUE_REGEX"
|
|
||||||
name="value"
|
name="value"
|
||||||
ng-change="$ctrl.onChangeValue($ctrl.variable.value)"
|
ng-change="$ctrl.onChangeValue($ctrl.variable.value)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
export const KEY_REGEX = /[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?/.source;
|
export const KEY_REGEX = /(.+)/.source;
|
||||||
|
|
||||||
export const VALUE_REGEX = /(.*)?/.source;
|
export const VALUE_REGEX = /(.*)?/.source;
|
||||||
|
|
||||||
const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);
|
const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);
|
||||||
@@ -16,7 +15,7 @@ export function parseDotEnvFile(src) {
|
|||||||
return parseArrayOfStrings(
|
return parseArrayOfStrings(
|
||||||
_.compact(src.split(NEWLINES_REGEX))
|
_.compact(src.split(NEWLINES_REGEX))
|
||||||
.map((v) => v.trim())
|
.map((v) => v.trim())
|
||||||
.filter((v) => !v.startsWith('#'))
|
.filter((v) => !v.startsWith('#') && v !== '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ export function parseArrayOfStrings(array) {
|
|||||||
|
|
||||||
const parsedKeyValArr = variableString.trim().match(KEY_VALUE_REGEX);
|
const parsedKeyValArr = variableString.trim().match(KEY_VALUE_REGEX);
|
||||||
if (parsedKeyValArr != null && parsedKeyValArr.length > 4) {
|
if (parsedKeyValArr != null && parsedKeyValArr.length > 4) {
|
||||||
return { name: parsedKeyValArr[1], value: parsedKeyValArr[3] || '' };
|
return { name: parsedKeyValArr[1].trim(), value: parsedKeyValArr[3].trim() || '' };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ angular.module('portainer.app').factory('ChartService', [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
hover: { animationDuration: 0 },
|
hover: { animationDuration: 0 },
|
||||||
scales: {
|
scales: {
|
||||||
yAxes: [
|
yAxes: [
|
||||||
|
|||||||
+4
-4
@@ -191,7 +191,7 @@ function shell_download_docker_binary(p, a) {
|
|||||||
var ip = ps[p] === undefined ? p : ps[p];
|
var ip = ps[p] === undefined ? p : ps[p];
|
||||||
var ia = as[a] === undefined ? a : as[a];
|
var ia = as[a] === undefined ? a : as[a];
|
||||||
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>';
|
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'if [ -f dist/docker ] || [ -f dist/docker.exe ]; then',
|
'if [ -f dist/docker ] || [ -f dist/docker.exe ]; then',
|
||||||
'echo "docker binary exists";',
|
'echo "docker binary exists";',
|
||||||
@@ -207,7 +207,7 @@ function shell_download_docker_compose_binary(p, a) {
|
|||||||
var ip = ps[p] || p;
|
var ip = ps[p] || p;
|
||||||
var ia = as[a] || a;
|
var ia = as[a] || a;
|
||||||
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsComposeVersion %>' : '<%= binaries.dockerLinuxComposeVersion %>';
|
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsComposeVersion %>' : '<%= binaries.dockerLinuxComposeVersion %>';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then',
|
'if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then',
|
||||||
'echo "Docker Compose binary exists";',
|
'echo "Docker Compose binary exists";',
|
||||||
@@ -219,7 +219,7 @@ function shell_download_docker_compose_binary(p, a) {
|
|||||||
|
|
||||||
function shell_download_kompose_binary(p, a) {
|
function shell_download_kompose_binary(p, a) {
|
||||||
var binaryVersion = '<%= binaries.komposeVersion %>';
|
var binaryVersion = '<%= binaries.komposeVersion %>';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'if [ -f dist/kompose ] || [ -f dist/kompose.exe ]; then',
|
'if [ -f dist/kompose ] || [ -f dist/kompose.exe ]; then',
|
||||||
'echo "kompose binary exists";',
|
'echo "kompose binary exists";',
|
||||||
@@ -231,7 +231,7 @@ function shell_download_kompose_binary(p, a) {
|
|||||||
|
|
||||||
function shell_download_kubectl_binary(p, a) {
|
function shell_download_kubectl_binary(p, a) {
|
||||||
var binaryVersion = '<%= binaries.kubectlVersion %>';
|
var binaryVersion = '<%= binaries.kubectlVersion %>';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'if [ -f dist/kubectl ] || [ -f dist/kubectl.exe ]; then',
|
'if [ -f dist/kubectl ] || [ -f dist/kubectl.exe ]; then',
|
||||||
'echo "kubectl binary exists";',
|
'echo "kubectl binary exists";',
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "2.6.0",
|
"version": "2.6.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
|||||||
Reference in New Issue
Block a user