Compare commits

...

18 Commits

Author SHA1 Message Date
Stéphane Busso
1fb5d31f7e Bump to 2.6.3 2021-08-27 09:25:49 +12:00
LP B
9c616ffb07 feat(app/k8s): update ingress scheme from v1beta1 to v1 (#5466) 2021-08-25 18:35:03 +12:00
yi-portainer
3254051647 * update version to 2.6.2 2021-07-30 10:28:09 +12:00
Matt Hook
a0b52fc3d7 Fixes for EE-1035 and dockerhub pro accounts. (#5343) 2021-07-27 10:41:58 +12:00
cong meng
31fdef1e60 fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes (#5324)
* fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes

* fix(advance deploy): EE-1141 reuse existing token cache when do deployment

* fix: EE-1141 use user's SA token to exec pod command

* fix: EE-1141 stop advanced-deploy or pod-exec if user's SA token is empty

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-27 09:55:09 +12:00
Hui
be30e1c453 fix(swagger): add swagger annotation for pull and redeploy stack 2021-07-22 11:39:47 +12:00
Richard Wei
5b55b890e7 fix charts x label padding (#5339) 2021-07-21 13:54:26 +12:00
Dmitry Salakhov
a5eac07b0c fix(namespace): update portainer-config when delete a namespace (#5328) 2021-07-20 14:05:40 +12:00
fhanportainer
fa80a7b7e5 fix(k8s): fixed generating kube auction summary issue (#5332) 2021-07-19 19:45:14 +12:00
cong meng
278667825a EE-1110 Ingress routes and their mapping to a application name are not deleted when the application is deleted (#5291)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-09 10:39:14 +12:00
cong meng
65ded647b6 fix(ingress): fixed hostname field when having multiple ingresses EE-1072 (#5273) (#5285)
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-07-08 12:08:20 +12:00
Richard Wei
084cdcd8dc fix(app):Set resource assignment default to off EE-1043 (#5286) 2021-07-08 12:08:10 +12:00
Stéphane Busso
5b68c4365e Merge branch 'release/2.6' of github.com:portainer/portainer into release/2.6 2021-07-08 11:39:21 +12:00
Stéphane Busso
9cd64664cc fix download logs (#5243) 2021-07-08 11:37:18 +12:00
yi-portainer
e831fa4a03 * update versions to 2.6.1 2021-07-07 17:20:18 +12:00
cong meng
2a3c807978 fix(ingress): EE-1049 Ingress config is lost when deleting an application deployed with ingress (#5264)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-07 14:08:20 +12:00
cong meng
a8265a44d0 fix EE-1078 Too strict form validation for docker environment variables (#5278)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-07 12:52:37 +12:00
Hui
71ad21598b remove expiry time copy logic (#5259) 2021-06-30 16:49:48 +12:00
48 changed files with 561 additions and 235 deletions

View File

@@ -89,8 +89,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath)
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
}
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)
kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)

View File

@@ -5,6 +5,9 @@ import (
"encoding/json"
"errors"
"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"
"net/http"
"net/url"
@@ -20,27 +23,64 @@ import (
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
binaryPath string
dataStore portainer.DataStore
reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
}
// 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{
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
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.
// 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 {
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
token, err := deployer.getToken(request, endpoint, true);
if err != nil {
return "", err
}
@@ -53,7 +93,7 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
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, "apply", "-f", "-")
@@ -139,8 +179,14 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo
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.PortainerAgentSignatureHeader, signature)
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
resp, err := httpCli.Do(req)
if err != nil {

View File

@@ -1,7 +1,10 @@
package gittypes
type RepoConfig struct {
URL string
ReferenceName string
ConfigFilePath string
// The repo url
URL string `example:"https://github.com/portainer/portainer-ee.git"`
// 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"`
}

View File

@@ -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/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
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/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
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/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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
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.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/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.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
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 v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
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/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
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 v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
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/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=

View File

@@ -5,7 +5,6 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/asaskevich/govalidator"
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))
}
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 {
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {

View File

@@ -4,7 +4,6 @@ import (
"errors"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -26,21 +25,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
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 == "" {
return "", nil, errors.New("Invalid OAuth authorization code")
return "", errors.New("Invalid OAuth authorization code")
}
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 {
return "", nil, err
return "", err
}
return username, expiryTime, nil
return username, nil
}
// @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")}
}
username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
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)
}

View File

@@ -115,8 +115,18 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
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")
if err != nil {
return nil, nil
}
rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining")
if err != nil {
return nil, nil
}
return &dockerhubStatusResponse{
Limit: rateLimit,

View File

@@ -67,7 +67,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.1.1
// @version 2.6.3
// @description.markdown api-description.md
// @termsOfService

View File

@@ -95,7 +95,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
doCleanUp := true
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 {
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}
}
output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil {
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)
}
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()
defer handler.stackCreationMutex.Unlock()
@@ -167,7 +167,7 @@ func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stac
stackConfig = string(convertedConfig)
}
return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace)
return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace)
}

View File

@@ -27,7 +27,7 @@ import (
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 " not found"
// @failure 404 "Not found"
// @failure 500 "Server error"
// @router /stacks/{id} [delete]
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -26,7 +26,7 @@ import (
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 " not found"
// @failure 404 "Not found"
// @failure 500 "Server error"
// @router /stacks/{id}/start [post]
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -24,7 +24,7 @@ import (
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 " not found"
// @failure 404 "Not found"
// @failure 500 "Server error"
// @router /stacks/{id}/stop [post]
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -33,7 +33,23 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error {
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 {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"github.com/gorilla/websocket"
httperror "github.com/portainer/libhttp/error"
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/kubernetes/cli"
)
@@ -12,20 +13,22 @@ import (
// Handler is the HTTP handler used to handle websocket operations.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
KubernetesClientFactory *cli.ClientFactory
requestBouncer *security.RequestBouncer
connectionUpgrader websocket.Upgrader
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
KubernetesClientFactory *cli.ClientFactory
requestBouncer *security.RequestBouncer
connectionUpgrader websocket.Upgrader
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
}
// 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{
Router: mux.NewRouter(),
connectionUpgrader: websocket.Upgrader{},
requestBouncer: bouncer,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
}
h.PathPrefix("/websocket/exec").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))

View File

@@ -1,6 +1,8 @@
package websocket
import (
"fmt"
"github.com/portainer/portainer/api/http/security"
"io"
"log"
"net/http"
@@ -11,6 +13,7 @@ import (
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
)
// @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}
}
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{
endpoint: endpoint,
token: token,
}
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}
}
err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
err = cli.StartExecProcess(token, useAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
if err != nil {
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
}
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
}

View File

@@ -24,6 +24,7 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
proxy.Director = func(incoming *http.Request, out http.Header) {
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
}
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.PortainerAgentSignatureHeader, signature)
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
}
proxy.ServeHTTP(w, r)

View File

@@ -8,4 +8,5 @@ type webSocketRequestParams struct {
ID string
nodeName string
endpoint *portainer.Endpoint
token string
}

View File

@@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
return nil, err
}
transport, err := kubernetes.NewLocalTransport(tokenManager)
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.dataStore)
if err != nil {
return nil, err
}
@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
endpointURL.Scheme = "http"
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
}
@@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
}
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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
package kubernetes
import (
"io/ioutil"
"sync"
portainer "github.com/portainer/portainer/api"
"io/ioutil"
)
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
@@ -13,7 +11,6 @@ type tokenManager struct {
tokenCache *tokenCache
kubecli portainer.KubeClient
dataStore portainer.DataStore
mutex sync.Mutex
adminToken string
}
@@ -25,7 +22,6 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
tokenCache: cache,
kubecli: kubecli,
dataStore: dataStore,
mutex: sync.Mutex{},
adminToken: "",
}
@@ -41,13 +37,13 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore
return tokenManager, nil
}
func (manager *tokenManager) getAdminServiceAccountToken() string {
func (manager *tokenManager) GetAdminServiceAccountToken() string {
return manager.adminToken
}
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
manager.mutex.Lock()
defer manager.mutex.Unlock()
func (manager *tokenManager) GetUserServiceAccountToken(userID int) (string, error) {
manager.tokenCache.mutex.Lock()
defer manager.tokenCache.mutex.Unlock()
token, ok := manager.tokenCache.getToken(userID)
if !ok {

View File

@@ -2,6 +2,7 @@ package kubernetes
import (
"strconv"
"sync"
"github.com/orcaman/concurrent-map"
)
@@ -14,6 +15,7 @@ type (
tokenCache struct {
userTokenCache cmap.ConcurrentMap
mutex sync.Mutex
}
)
@@ -35,6 +37,18 @@ func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *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.
func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
for cache := range manager.tokenCaches.IterBuffered() {
@@ -45,6 +59,7 @@ func (manager *TokenCacheManager) RemoveUserFromCache(userID int) {
func newTokenCache() *tokenCache {
return &tokenCache{
userTokenCache: cmap.New(),
mutex: sync.Mutex{},
}
}

View File

@@ -2,147 +2,73 @@ package kubernetes
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"github.com/portainer/portainer/api/http/security"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
}
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
type baseTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpoint *portainer.Endpoint
dataStore portainer.DataStore
}
// 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.endpointIdentifier)
if err != nil {
return nil, err
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) *baseTransport {
return &baseTransport{
httpTransport: httpTransport,
tokenManager: tokenManager,
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)
}
// 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) *agentTransport {
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
}
var (
namespaceRegex = regexp.MustCompile(`^/namespaces/([^/]*)$`)
)
func getRoundTripToken(
request *http.Request,
@@ -156,9 +82,9 @@ func getRoundTripToken(
var token string
if tokenData.Role == portainer.AdministratorRole {
token = tokenManager.getAdminServiceAccountToken()
token = tokenManager.GetAdminServiceAccountToken()
} else {
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID))
if err != nil {
log.Printf("Failed retrieving service account token: %v", err)
return "", err

View File

@@ -204,7 +204,7 @@ func (server *Server) Start() error {
userHandler.DataStore = server.DataStore
userHandler.CryptoService = server.CryptoService
var websocketHandler = websocket.NewHandler(requestBouncer)
var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer)
websocketHandler.DataStore = server.DataStore
websocketHandler.SignatureService = server.SignatureService
websocketHandler.ReverseTunnelService = server.ReverseTunnelService

View File

@@ -3,6 +3,7 @@ package cli
import (
"encoding/json"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -80,6 +81,21 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
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
// from config maps in the portainer namespace
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {

View File

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

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"strconv"
"sync"
cmap "github.com/orcaman/concurrent-map"
@@ -25,8 +26,9 @@ type (
// KubeClient represent a service used to execute Kubernetes operations
KubeClient struct {
cli *kubernetes.Clientset
cli kubernetes.Interface
instanceID string
lock *sync.Mutex
}
)
@@ -72,6 +74,7 @@ func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (po
kubecli := &KubeClient{
cli: cli,
instanceID: factory.instanceID,
lock: &sync.Mutex{},
}
return kubecli, nil

View File

@@ -14,13 +14,18 @@ import (
// 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
// to the stdout parameter.
// This function only works against a local endpoint using an in-cluster config.
func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
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()
if err != nil {
return err
}
if !useAdminToken {
config.BearerToken = token
config.BearerTokenFile = ""
}
req := kcl.cli.CoreV1().RESTClient().
Post().
Resource("pods").

View File

@@ -9,7 +9,6 @@ import (
"mime"
"net/http"
"net/url"
"time"
"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.
// 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.
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)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
return "", nil, err
return "", err
}
username, err := getUsername(token.AccessToken, configuration)
if err != nil {
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) {

View File

@@ -2,6 +2,7 @@ package portainer
import (
"io"
"net/http"
"time"
gittypes "github.com/portainer/portainer/api/git/types"
@@ -1164,14 +1165,15 @@ type (
KubeClient interface {
SetupUserServiceAccount(userID int, teamIDs []int) 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)
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
}
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
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)
}
@@ -1189,7 +1191,7 @@ type (
// OAuthService represents a service used to authenticate users using OAuth
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
@@ -1341,7 +1343,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.6.0"
APIVersion = "2.6.3"
// DBVersion is the version number of the Portainer database
DBVersion = 30
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -19,7 +19,7 @@ export default class porImageRegistryContainerController {
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
try {
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) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);

View File

@@ -48,7 +48,7 @@ angular.module('portainer.docker').controller('LogViewerController', [
};
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');
};
},

View File

@@ -261,7 +261,7 @@ class KubernetesApplicationConverter {
return res;
}
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) {
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels, ingresses) {
const res = new KubernetesApplicationFormValues();
res.ApplicationType = app.ApplicationType;
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.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
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;
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;

View File

@@ -274,7 +274,7 @@ class KubernetesApplicationHelper {
/* #endregion */
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) {
const generatePort = (port, rule) => {
const res = new KubernetesApplicationPublishedPortFormValue();
res.IsNew = false;
@@ -282,6 +282,7 @@ class KubernetesApplicationHelper {
res.IngressName = rule.IngressName;
res.IngressRoute = rule.Path;
res.IngressHost = rule.Host;
res.IngressHosts = ingress && ingress.find((i) => i.Name === rule.IngressName).Hosts;
}
res.Protocol = port.Protocol;
res.ContainerPort = port.TargetPort;

View File

@@ -19,10 +19,10 @@ export class KubernetesIngressConverter {
: _.map(rule.http.paths, (path) => {
const ingRule = new KubernetesIngressRule();
ingRule.IngressName = data.metadata.name;
ingRule.ServiceName = path.backend.serviceName;
ingRule.ServiceName = path.backend.service.name;
ingRule.Host = rule.host || '';
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
ingRule.Port = path.backend.servicePort;
ingRule.Port = path.backend.service.port.number;
ingRule.Path = path.path;
return ingRule;
});
@@ -57,7 +57,9 @@ export class KubernetesIngressConverter {
rule.IngressName = ingress.Name;
rule.ServiceName = serviceName;
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;
ingress.Paths.push(rule);
}
@@ -149,8 +151,8 @@ export class KubernetesIngressConverter {
rule.http.paths = _.map(paths, (p) => {
const path = new KubernetesIngressRulePathCreatePayload();
path.path = p.Path;
path.backend.serviceName = p.ServiceName;
path.backend.servicePort = p.Port;
path.backend.service.name = p.ServiceName;
path.backend.service.port.number = p.Port;
return path;
});
hostsWithRules.push(host);
@@ -171,7 +173,7 @@ export class KubernetesIngressConverter {
res.spec.rules = [];
_.forEach(data.Hosts, (host) => {
if (!host.NeedsDeletion) {
res.spec.rules.push({ host: host.Host });
res.spec.rules.push({ host: host.Host || host });
}
});
} else {

View File

@@ -20,10 +20,15 @@ export function KubernetesIngressRuleCreatePayload() {
export function KubernetesIngressRulePathCreatePayload() {
return {
backend: {
serviceName: '',
servicePort: 0,
},
path: '',
pathType: 'ImplementationSpecific',
backend: {
service: {
name: '',
port: {
number: 0,
},
},
},
};
}

View File

@@ -5,7 +5,7 @@ angular.module('portainer.kubernetes').factory('KubernetesIngresses', factory);
function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`;
const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`;
return $resource(
url,
{

View File

@@ -124,6 +124,7 @@ export function KubernetesApplicationPublishedPortFormValue() {
IngressName: undefined,
IngressRoute: undefined,
IngressHost: undefined,
IngressHosts: [],
};
}

View File

@@ -1368,7 +1368,7 @@
class="form-control"
name="ingress_hostname_{{ $index }}"
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-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
>

View File

@@ -321,6 +321,7 @@ class KubernetesCreateApplicationController {
const ingresses = this.filteredIngresses;
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : 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) {
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
}
@@ -388,6 +389,7 @@ class KubernetesCreateApplicationController {
onChangePortMappingIngress(index) {
const publishedPort = this.formValues.PublishedPorts[index];
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
publishedPort.IngressHosts = ingress.Hosts;
this.ingressHostnames = ingress.Hosts;
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
this.onChangePublishedPorts();
@@ -972,7 +974,8 @@ class KubernetesCreateApplicationController {
this.resourcePools,
this.configurations,
this.persistentVolumeClaims,
this.nodesLabels
this.nodesLabels,
this.filteredIngresses
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);

View File

@@ -172,6 +172,7 @@ class KubernetesCreateResourcePoolController {
this.endpoint = endpoint;
this.defaults = KubernetesResourceQuotaDefaults;
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
this.formValues.HasQuota = true;
this.state = {
actionInProgress: false,

View File

@@ -1,12 +1,4 @@
import { KEY_REGEX, VALUE_REGEX } from '@/portainer/helpers/env-vars';
class EnvironmentVariablesSimpleModeItemController {
/* @ngInject */
constructor() {
this.KEY_REGEX = KEY_REGEX;
this.VALUE_REGEX = VALUE_REGEX;
}
onChangeName(name) {
const fieldIsInvalid = typeof name === 'undefined';
if (fieldIsInvalid) {

View File

@@ -9,7 +9,6 @@
placeholder="e.g. FOO"
ng-model="$ctrl.variable.name"
ng-disabled="$ctrl.variable.added"
ng-pattern="$ctrl.KEY_REGEX"
ng-change="$ctrl.onChangeName($ctrl.variable.name)"
required
/>
@@ -36,7 +35,6 @@
ng-model="$ctrl.variable.value"
placeholder="e.g. bar"
ng-trim="false"
ng-pattern="$ctrl.VALUE_REGEX"
name="value"
ng-change="$ctrl.onChangeValue($ctrl.variable.value)"
/>

View File

@@ -1,7 +1,6 @@
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;
const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);
@@ -16,7 +15,7 @@ export function parseDotEnvFile(src) {
return parseArrayOfStrings(
_.compact(src.split(NEWLINES_REGEX))
.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);
if (parsedKeyValArr != null && parsedKeyValArr.length > 4) {
return { name: parsedKeyValArr[1], value: parsedKeyValArr[3] || '' };
return { name: parsedKeyValArr[1].trim(), value: parsedKeyValArr[3].trim() || '' };
}
})
);

View File

@@ -26,6 +26,11 @@ angular.module('portainer.app').factory('ChartService', [
},
},
},
layout: {
padding: {
left: 15,
},
},
hover: { animationDuration: 0 },
scales: {
yAxes: [

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.6.0",
"version": "2.6.3",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"