Compare commits

..

11 Commits

Author SHA1 Message Date
Simon Meng
e421e04dd1 fix(stack) ignore username and password when authentication is disabled EE-161 2021-06-22 13:30:54 +12:00
Chaim Lev-Ari
a0e3129f0d fix(stacks): clear directory if success 2021-06-20 12:43:35 +03:00
Chaim Lev-Ari
f487a067c7 feat(stacks): don't accept prune and env on update git 2021-06-11 07:23:49 +03:00
Chaim Lev-Ari
37dd521b9e feat(stacks): backup stack folder before cloning 2021-06-11 07:23:26 +03:00
Chaim Lev-Ari
27cfb692f4 feat(stacks): set stopped stacks status to activate when updating 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
4b054f9dab fix(stacks): reload state when deployed 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
6cbbba596a fix(stacks): show same size description 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
a5c5d34de8 feat(stack): show loading after confirmation 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
75da9ac2aa feat(stacks): redeploy git stacks form
[EE-666]
2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
fea8ea6c73 feat(stacks): redeploy git stack api (#5112) 2021-06-06 13:02:24 +03:00
Chaim Lev-Ari
d982f89167 feat(git): save git config when creating stack (#5048)
* feat(git): save git config when creating stack

* chore(fs): test fileExists

* fix(git): fix tests to use CloneRepository

* refactor(git): move options to new object
2021-06-06 13:02:24 +03:00
156 changed files with 869 additions and 3794 deletions

View File

@@ -4,6 +4,7 @@ about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--

View File

@@ -4,8 +4,8 @@ about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
---
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.

View File

@@ -1,18 +0,0 @@
package migrator
func (m *Migrator) migrateDBVersionTo30() error {
if err := m.migrateSettings(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateSettings() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.OAuthSettings.SSO = false
legacySettings.OAuthSettings.LogoutURI = ""
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -1,95 +0,0 @@
package migrator
import (
"os"
"path"
"testing"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/settings"
)
var (
testingDBStorePath string
testingDBFileName string
dummyLogoURL string
dbConn *bolt.DB
settingsService *settings.Service
)
// initTestingDBConn creates a raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingDBConn(storePath, fileName string) (*bolt.DB, error) {
databasePath := path.Join(storePath, fileName)
dbConn, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
return dbConn, nil
}
// initTestingDBConn creates a settings service with raw bolt DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn *bolt.DB, preSetObj map[string]interface{}) (*settings.Service, error) {
internalDBConn := &internal.DbConnection{
DB: dbConn,
}
settingsService, err := settings.NewService(internalDBConn)
if err != nil {
return nil, err
}
//insert a obj
if err := internal.UpdateObject(internalDBConn, "settings", []byte("SETTINGS"), preSetObj); err != nil {
return nil, err
}
return settingsService, nil
}
func setup() error {
testingDBStorePath, _ = os.Getwd()
testingDBFileName = "portainer-ee-mig-30.db"
dummyLogoURL = "example.com"
var err error
dbConn, err = initTestingDBConn(testingDBStorePath, testingDBFileName)
if err != nil {
return err
}
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
settingsService, err = initTestingSettingsService(dbConn, dummySettingsObj)
if err != nil {
return err
}
return nil
}
func TestMigrateSettings(t *testing.T) {
if err := setup(); err != nil {
t.Errorf("failed to complete testing setups, err: %v", err)
}
defer dbConn.Close()
defer os.Remove(testingDBFileName)
m := &Migrator{
db: dbConn,
settingsService: settingsService,
}
if err := m.migrateSettings(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
updatedSettings, err := m.settingsService.Settings()
if err != nil {
t.Errorf("failed to retrieve the updated settings: %v", err)
}
if updatedSettings.LogoURL != dummyLogoURL {
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
if updatedSettings.OAuthSettings.SSO != false {
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
}

View File

@@ -358,13 +358,5 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
err := m.migrateDBVersionTo30()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
@@ -89,8 +88,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
}
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 initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(assetsPath)
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
@@ -166,7 +165,6 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -390,9 +388,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
snapshotService.Start()
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err)
@@ -402,7 +397,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
@@ -465,7 +460,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,

View File

@@ -2,234 +2,71 @@ package exec
import (
"bytes"
"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"
"os/exec"
"path"
"runtime"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// 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
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
binaryPath string
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
dataStore: datastore,
reverseTunnelService: reverseTunnelService,
signatureService: signatureService,
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
binaryPath: binaryPath,
}
}
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.
// If composeFormat is set to true, it will leverage the kompose binary to deploy a compose compliant manifest.
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := deployer.getToken(request, endpoint, true);
func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) {
if composeFormat {
convertedData, err := deployer.convertComposeData(data)
if err != nil {
return "", err
return nil, err
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", token)
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
return string(output), nil
data = string(convertedData)
}
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
}
httpCli := &http.Client{
Transport: transport,
}
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return "", err
return nil, err
}
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
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, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(data)
output, err := cmd.Output()
if err != nil {
return "", err
return nil, errors.New(stderr.String())
}
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
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 {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
return output, nil
}
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) {
command := path.Join(deployer.binaryPath, "kompose")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kompose.exe")

View File

@@ -31,8 +31,6 @@ const (
ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml"
// ManifestFileDefaultName represents the default name of a k8s manifest file.
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// PrivateKeyFile represents the name on disk of the file containing the private key.

View File

@@ -1,10 +1,7 @@
package gittypes
type RepoConfig struct {
// 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"`
URL string
ReferenceName string
ConfigFilePath string
}

View File

@@ -80,7 +80,6 @@ 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=
@@ -154,7 +153,6 @@ 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=
@@ -221,10 +219,8 @@ 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=
@@ -370,11 +366,9 @@ 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=
@@ -401,7 +395,6 @@ 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

@@ -130,13 +130,19 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
return handler.persistAndWriteToken(w, composeTokenData(user))
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
return handler.persistAndWriteToken(w, tokenData)
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
@@ -198,11 +204,3 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
}
return false
}
func composeTokenData(user *portainer.User) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
}

View File

@@ -25,6 +25,17 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
// @id AuthenticateOauth
// @summary Authenticate with OAuth
// @tags auth
// @accept json
// @produce json
// @param body body oauthPayload true "OAuth Credentials used for authentication"
// @success 200 {object} authenticateResponse "Success"
// @failure 400 "Invalid request"
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth/oauth/validate [post]
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
@@ -42,46 +53,35 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
return username, nil
}
// @id ValidateOAuth
// @summary Authenticate with OAuth
// @tags auth
// @accept json
// @produce json
// @param body body oauthPayload true "OAuth Credentials used for authentication"
// @success 200 {object} authenticateResponse "Success"
// @failure 400 "Invalid request"
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth/oauth/validate [post]
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload oauthPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve settings from the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if settings.AuthenticationMethod != portainer.AuthenticationOAuth {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
}
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}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
}
user, err := handler.DataStore.User().UserByUsername(username)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a user with the specified username from the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Account not created beforehand in Portainer and automatic user provisioning not enabled", Err: httperrors.ErrUnauthorized}
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", httperrors.ErrUnauthorized}
}
if user == nil {
@@ -92,7 +92,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
err = handler.DataStore.User().CreateUser(user)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist user inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
if settings.OAuthSettings.DefaultTeamID != 0 {
@@ -104,7 +104,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
err = handler.DataStore.TeamMembership().CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist team membership inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
}
}

View File

@@ -236,14 +236,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
customTemplate.ProjectPath = projectPath
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return nil, err
}

View File

@@ -212,14 +212,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return nil, err
}

View File

@@ -109,33 +109,12 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
}
}
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
}
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
}
if updateAuthorizations {
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == endpointGroup.ID {
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err = handler.AuthorizationService.CleanNAPWithOverridePolicies(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
}
}
}
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)

View File

@@ -1,7 +1,6 @@
package endpointgroups
import (
"github.com/portainer/portainer/api/internal/authorization"
"net/http"
"github.com/gorilla/mux"
@@ -13,7 +12,6 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
AuthorizationService *authorization.Service
DataStore portainer.DataStore
}

View File

@@ -156,7 +156,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @accept multipart/form-data
// @produce json
// @param Name formData string true "Name that will be used to identify this endpoint (example: my-endpoint)"
// @param EndpointCreationType formData integer true "Environment type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5)
// @param EndpointType formData integer true "Environment type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5)
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)"
// @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)"
// @param GroupID formData int false "Endpoint group identifier. If not specified will default to 1 (unassigned)."

View File

@@ -115,18 +115,8 @@ 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

@@ -155,14 +155,11 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.Kubernetes = *payload.Kubernetes
}
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
updateAuthorizations = true
endpoint.UserAccessPolicies = payload.UserAccessPolicies
}
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
updateAuthorizations = true
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
}
@@ -255,15 +252,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if updateAuthorizations {
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err = handler.AuthorizationService.CleanNAPWithOverridePolicies(endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}

View File

@@ -5,7 +5,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"net/http"
@@ -29,7 +28,6 @@ type Handler struct {
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
}
// NewHandler creates a handler to manage endpoint operations.

View File

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

View File

@@ -18,8 +18,6 @@ type publicSettingsResponse struct {
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
// The URL used for oauth login
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
// The URL used for oauth logout
OAuthLogoutURI string `json:"OAuthLogoutURI" example:"https://gitlab.com/oauth/logout"`
// Whether telemetry is enabled
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
}
@@ -36,32 +34,20 @@ type publicSettingsResponse struct {
func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve the settings from the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
}
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,
settings.OAuthSettings.RedirectURI,
settings.OAuthSettings.Scopes),
}
publicSettings := generatePublicSettings(settings)
return response.JSON(w, publicSettings)
}
func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResponse {
publicSettings := &publicSettingsResponse{
LogoURL: appSettings.LogoURL,
AuthenticationMethod: appSettings.AuthenticationMethod,
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
EnableTelemetry: appSettings.EnableTelemetry,
}
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
publicSettings.OAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
appSettings.OAuthSettings.AuthorizationURI,
appSettings.OAuthSettings.ClientID,
appSettings.OAuthSettings.RedirectURI,
appSettings.OAuthSettings.Scopes)
//control prompt=login param according to the SSO setting
if !appSettings.OAuthSettings.SSO {
publicSettings.OAuthLoginURI += "&prompt=login"
}
}
return publicSettings
}

View File

@@ -1,70 +0,0 @@
package settings
import (
"fmt"
"testing"
portainer "github.com/portainer/portainer/api"
)
const (
dummyOAuthClientID = "1a2b3c4d"
dummyOAuthScopes = "scopes"
dummyOAuthAuthenticationURI = "example.com/auth"
dummyOAuthRedirectURI = "example.com/redirect"
dummyOAuthLogoutURI = "example.com/logout"
)
var (
dummyOAuthLoginURI string
mockAppSettings *portainer.Settings
)
func setup() {
dummyOAuthLoginURI = fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s",
dummyOAuthAuthenticationURI,
dummyOAuthClientID,
dummyOAuthRedirectURI,
dummyOAuthScopes)
mockAppSettings = &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationOAuth,
OAuthSettings: portainer.OAuthSettings{
AuthorizationURI: dummyOAuthAuthenticationURI,
ClientID: dummyOAuthClientID,
Scopes: dummyOAuthScopes,
RedirectURI: dummyOAuthRedirectURI,
LogoutURI: dummyOAuthLogoutURI,
},
}
}
func TestGeneratePublicSettingsWithSSO(t *testing.T) {
setup()
mockAppSettings.OAuthSettings.SSO = true
publicSettings := generatePublicSettings(mockAppSettings)
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
}
if publicSettings.OAuthLoginURI != dummyOAuthLoginURI {
t.Errorf("wrong OAuthLoginURI when SSO is switched on, want: %s, got: %s", dummyOAuthLoginURI, publicSettings.OAuthLoginURI)
}
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
}
}
func TestGeneratePublicSettingsWithoutSSO(t *testing.T) {
setup()
mockAppSettings.OAuthSettings.SSO = false
publicSettings := generatePublicSettings(mockAppSettings)
if publicSettings.AuthenticationMethod != portainer.AuthenticationOAuth {
t.Errorf("wrong AuthenticationMethod, want: %d, got: %d", portainer.AuthenticationOAuth, publicSettings.AuthenticationMethod)
}
expectedOAuthLoginURI := dummyOAuthLoginURI + "&prompt=login"
if publicSettings.OAuthLoginURI != expectedOAuthLoginURI {
t.Errorf("wrong OAuthLoginURI when SSO is switched off, want: %s, got: %s", expectedOAuthLoginURI, publicSettings.OAuthLoginURI)
}
if publicSettings.OAuthLogoutURI != dummyOAuthLogoutURI {
t.Errorf("wrong OAuthLogoutURI, want: %s, got: %s", dummyOAuthLogoutURI, publicSettings.OAuthLogoutURI)
}
}

View File

@@ -237,11 +237,11 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View File

@@ -2,11 +2,7 @@ package stacks
import (
"errors"
"io/ioutil"
"net/http"
"path/filepath"
"strconv"
"time"
"github.com/asaskevich/govalidator"
@@ -14,29 +10,15 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
const defaultReferenceName = "refs/heads/master"
type kubernetesStringDeploymentPayload struct {
type kubernetesStackPayload struct {
ComposeFormat bool
Namespace string
StackFileContent string
}
type kubernetesGitDeploymentPayload struct {
ComposeFormat bool
Namespace string
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
FilePathInRepository string
}
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
func (payload *kubernetesStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
}
@@ -46,146 +28,32 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
return nil
}
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.FilePathInRepository) {
return errors.New("Invalid file path in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultReferenceName
}
return nil
}
type createKubernetesStackResponse struct {
Output string `json:"Output"`
}
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesStringDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
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}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
err = handler.DataStore.Stack().CreateStack(stack)
output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to deploy Kubernetes stack", err}
}
resp := &createKubernetesStackResponse{
Output: output,
Output: string(output),
}
return response.JSON(w, resp)
}
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
}
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}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
}
resp := &createKubernetesStackResponse{
Output: output,
}
return response.JSON(w, resp)
}
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) {
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
if composeFormat {
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(stackConfig)
if err != nil {
return "", err
}
stackConfig = string(convertedConfig)
}
return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace)
}
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
repositoryUsername := gitInfo.RepositoryUsername
repositoryPassword := gitInfo.RepositoryPassword
if !gitInfo.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err := handler.GitService.CloneRepository(projectPath, gitInfo.RepositoryURL, gitInfo.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(filepath.Join(projectPath, gitInfo.FilePathInRepository))
if err != nil {
return "", err
}
return string(content), nil
return handler.KubernetesDeployer.Deploy(endpoint, data, composeFormat, namespace)
}

View File

@@ -1,64 +0,0 @@
package stacks
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
type git struct {
content string
}
func (g *git) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func (g *git) ClonePublicRepository(repositoryURL string, referenceName string, destination string) error {
return ioutil.WriteFile(path.Join(destination, "deployment.yml"), []byte(g.content), 0755)
}
func (g *git) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func TestCloneAndConvertGitRepoFile(t *testing.T) {
dir, err := os.MkdirTemp("", "kube-create-stack")
assert.NoError(t, err, "failed to create a tmp dir")
defer os.RemoveAll(dir)
content := `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80`
h := &Handler{
GitService: &git{
content: content,
},
}
gitInfo := &kubernetesGitDeploymentPayload{
FilePathInRepository: "deployment.yml",
}
fileContent, err := h.cloneManifestContentFromGitRepo(gitInfo, dir)
assert.NoError(t, err, "failed to clone or convert the file from Git repo")
assert.Equal(t, content, fileContent)
}

View File

@@ -52,8 +52,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
h.Handle("/stacks/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
h.Handle("/stacks/{id}/associate",
bouncer.AdminAccess(httperror.LoggerHandler(h.stackAssociate))).Methods(http.MethodPut)
h.Handle("/stacks/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
h.Handle("/stacks/{id}/git",

View File

@@ -1,91 +0,0 @@
package stacks
import (
"fmt"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
"net/http"
"time"
)
// PUT request on /api/stacks/:id/associate?endpointId=<endpointId>&swarmId=<swarmId>&orphanedRunning=<orphanedRunning>
func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
swarmId, err := request.RetrieveQueryParameter(r, "swarmId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: swarmId", err}
}
orphanedRunning, err := request.RetrieveBooleanQueryParameter(r, "orphanedRunning", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: orphanedRunning", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if resourceControl != nil {
resourceControl.ResourceID = fmt.Sprintf("%d_%s", endpointID, stack.Name)
err = handler.DataStore.ResourceControl().UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control changes inside the database", err}
}
}
stack.EndpointID = portainer.EndpointID(endpointID)
stack.SwarmID = swarmId
if orphanedRunning {
stack.Status = portainer.StackStatusActive
} else {
stack.Status = portainer.StackStatusInactive
}
stack.CreationDate = time.Now().Unix()
stack.CreatedBy = user.Username
stack.UpdateDate = 0
stack.UpdatedBy = ""
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
stack.ResourceControl = resourceControl
return response.JSON(w, stack)
}

View File

@@ -14,9 +14,9 @@ import (
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -78,7 +78,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
@@ -112,7 +112,11 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
case portainer.DockerComposeStack:
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack:
return handler.createKubernetesStack(w, r, method, endpoint)
if tokenData.Role != portainer.AdministratorRole {
return &httperror.HandlerError{http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized}
}
return handler.createKubernetesStack(w, r, endpoint)
}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
@@ -145,16 +149,6 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch method {
case "string":
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
case "repository":
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
@@ -236,10 +230,6 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
}
func (handler *Handler) cloneAndSaveConfig(stack *portainer.Stack, projectPath, repositoryURL, refName, configFilePath string, auth bool, username, password string) error {
if !auth {
username = ""
password = ""
}
err := handler.GitService.CloneRepository(projectPath, repositoryURL, refName, username, password)
if err != nil {

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 {
@@ -58,42 +58,42 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, this API endpoint
// can use the optional EndpointID query parameter to set a valid endpoint identifier to be
// used in the context of this request.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
if isOrphaned && !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")}
endpointIdentifier := stack.EndpointID
if endpointID != 0 {
endpointIdentifier = portainer.EndpointID(endpointID)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointIdentifier)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if !isOrphaned {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
err = handler.deleteStack(stack, endpoint)

View File

@@ -46,38 +46,34 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
if !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))

View File

@@ -1,7 +1,6 @@
package stacks
import (
"github.com/portainer/portainer/api/http/errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -9,6 +8,7 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -40,42 +40,38 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
if !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
}
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
if resourceControl != nil {
stack.ResourceControl = resourceControl
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {
stack.ResourceControl = resourceControl
}
return response.JSON(w, stack)

View File

@@ -1,7 +1,6 @@
package stacks
import (
httperrors "github.com/portainer/portainer/api/http/errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -13,9 +12,8 @@ import (
)
type stackListOperationFilters struct {
SwarmID string `json:"SwarmID"`
EndpointID int `json:"EndpointID"`
IncludeOrphanedStacks bool `json:"IncludeOrphanedStacks"`
SwarmID string `json:"SwarmID"`
EndpointID int `json:"EndpointID"`
}
// @id StackList
@@ -39,16 +37,11 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err}
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
stacks = filterStacks(stacks, &filters, endpoints)
stacks = filterStacks(stacks, &filters)
resourceControls, err := handler.DataStore.ResourceControl().ResourceControls()
if err != nil {
@@ -63,10 +56,6 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
stacks = authorization.DecorateStacks(stacks, resourceControls)
if !securityContext.IsAdmin {
if filters.IncludeOrphanedStacks {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access orphaned stacks", httperrors.ErrUnauthorized}
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
@@ -83,20 +72,13 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
return response.JSON(w, stacks)
}
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters, endpoints []portainer.Endpoint) []portainer.Stack {
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
if filters.EndpointID == 0 && filters.SwarmID == "" {
return stacks
}
filteredStacks := make([]portainer.Stack, 0, len(stacks))
for _, stack := range stacks {
if filters.IncludeOrphanedStacks && isOrphanedStack(stack, endpoints) {
if (stack.Type == portainer.DockerComposeStack && filters.SwarmID == "") || (stack.Type == portainer.DockerSwarmStack && filters.SwarmID != "") {
filteredStacks = append(filteredStacks, stack)
}
continue
}
if stack.Type == portainer.DockerComposeStack && stack.EndpointID == portainer.EndpointID(filters.EndpointID) {
filteredStacks = append(filteredStacks, stack)
}
@@ -107,13 +89,3 @@ func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters,
return filteredStacks
}
func isOrphanedStack(stack portainer.Stack, endpoints []portainer.Endpoint) bool {
for _, endpoint := range endpoints {
if stack.EndpointID == endpoint.ID {
return false
}
}
return true
}

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,23 +33,7 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error {
return nil
}
// @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]
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -5,7 +5,6 @@ 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"
)
@@ -13,22 +12,20 @@ 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
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
KubernetesClientFactory *cli.ClientFactory
requestBouncer *security.RequestBouncer
connectionUpgrader websocket.Upgrader
}
// NewHandler creates a handler to manage websocket operations.
func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bouncer *security.RequestBouncer) *Handler {
func NewHandler(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,8 +1,6 @@
package websocket
import (
"fmt"
"github.com/portainer/portainer/api/http/security"
"io"
"log"
"net/http"
@@ -13,7 +11,6 @@ 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
@@ -73,14 +70,8 @@ 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")
@@ -121,7 +112,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(token, useAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
}
@@ -133,37 +124,3 @@ 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,7 +24,6 @@ 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)
@@ -65,7 +64,6 @@ 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,5 +8,4 @@ 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, endpoint, factory.dataStore)
transport, err := kubernetes.NewLocalTransport(tokenManager)
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, tokenManager)
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, 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, endpoint)
proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager)
return proxy, nil
}

View File

@@ -1,55 +0,0 @@
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

@@ -1,52 +0,0 @@
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

@@ -1,46 +0,0 @@
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

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

View File

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

View File

@@ -2,73 +2,147 @@ 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 baseTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpoint *portainer.Endpoint
dataStore portainer.DataStore
}
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]
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
}
switch {
case requestPath == "" && request.Method == "DELETE":
return transport.deleteNamespaceRequest(request, namespace)
default:
return transport.executeKubernetesRequest(request, true)
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
}
func (transport *baseTransport) executeKubernetesRequest(request *http.Request, shouldLog bool) (*http.Response, error) {
// 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
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return transport.httpTransport.RoundTrip(request)
}
var (
namespaceRegex = regexp.MustCompile(`^/namespaces/([^/]*)$`)
)
// 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
}
func getRoundTripToken(
request *http.Request,
@@ -82,9 +156,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

@@ -45,13 +45,11 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Server implements the portainer.Server interface
type Server struct {
AuthorizationService *authorization.Service
BindAddress string
AssetsPath string
Status *portainer.Status
@@ -137,7 +135,6 @@ func (server *Server) Start() error {
endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager
endpointHandler.AuthorizationService = server.AuthorizationService
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.DataStore = server.DataStore
@@ -145,7 +142,6 @@ func (server *Server) Start() error {
endpointEdgeHandler.ReverseTunnelService = server.ReverseTunnelService
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.AuthorizationService = server.AuthorizationService
endpointGroupHandler.DataStore = server.DataStore
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
@@ -204,7 +200,7 @@ func (server *Server) Start() error {
userHandler.DataStore = server.DataStore
userHandler.CryptoService = server.CryptoService
var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer)
var websocketHandler = websocket.NewHandler(requestBouncer)
websocketHandler.DataStore = server.DataStore
websocketHandler.SignatureService = server.SignatureService
websocketHandler.ReverseTunnelService = server.ReverseTunnelService

View File

@@ -1,15 +1,11 @@
package authorization
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli"
)
import "github.com/portainer/portainer/api"
// Service represents a service used to
// update authorizations associated to a user or team.
type Service struct {
dataStore portainer.DataStore
K8sClientFactory *cli.ClientFactory
}
// NewService returns a point to a new Service instance.

View File

@@ -1,134 +0,0 @@
package authorization
import portainer "github.com/portainer/portainer/api"
// CleanNAPWithOverridePolicies Clean Namespace Access Policies with override policies
func (service *Service) CleanNAPWithOverridePolicies(
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) error {
kubecli, err := service.K8sClientFactory.GetKubeClient(endpoint)
if err != nil {
return err
}
accessPolicies, err := kubecli.GetNamespaceAccessPolicies()
if err != nil {
return err
}
hasChange := false
for namespace, policy := range accessPolicies {
for teamID := range policy.TeamAccessPolicies {
access, err := service.getTeamEndpointAccessWithPolicies(teamID, endpoint, endpointGroup)
if err != nil {
return err
}
if !access {
delete(accessPolicies[namespace].TeamAccessPolicies, teamID)
hasChange = true
}
}
for userID := range policy.UserAccessPolicies {
access, err := service.getUserEndpointAccessWithPolicies(userID, endpoint, endpointGroup)
if err != nil {
return err
}
if !access {
delete(accessPolicies[namespace].UserAccessPolicies, userID)
hasChange = true
}
}
}
if hasChange {
err = kubecli.UpdateNamespaceAccessPolicies(accessPolicies)
if err != nil {
return err
}
}
return nil
}
func (service *Service) getUserEndpointAccessWithPolicies(
userID portainer.UserID,
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) (bool, error) {
memberships, err := service.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
if err != nil {
return false, err
}
if endpointGroup == nil {
endpointGroup, err = service.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
return false, err
}
}
if userAccess(userID, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies, memberships) {
return true, nil
}
if userAccess(userID, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies, memberships) {
return true, nil
}
return false, nil
}
func userAccess(
userID portainer.UserID,
userAccessPolicies portainer.UserAccessPolicies,
teamAccessPolicies portainer.TeamAccessPolicies,
memberships []portainer.TeamMembership,
) bool {
if _, ok := userAccessPolicies[userID]; ok {
return true
}
for _, membership := range memberships {
if _, ok := teamAccessPolicies[membership.TeamID]; ok {
return true
}
}
return false
}
func (service *Service) getTeamEndpointAccessWithPolicies(
teamID portainer.TeamID,
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) (bool, error) {
if endpointGroup == nil {
var err error
endpointGroup, err = service.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
return false, err
}
}
if teamAccess(teamID, endpoint.TeamAccessPolicies) {
return true, nil
}
if teamAccess(teamID, endpointGroup.TeamAccessPolicies) {
return true, nil
}
return false, nil
}
func teamAccess(
teamID portainer.TeamID,
teamAccessPolicies portainer.TeamAccessPolicies,
) bool {
_, ok := teamAccessPolicies[teamID];
return ok
}

View File

@@ -1,17 +0,0 @@
package endpoint
import portainer "github.com/portainer/portainer/api"
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
}
// IsDocketEndpoint returns true if this is a docker endpoint
func IsDocketEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.DockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
}

View File

@@ -9,17 +9,3 @@ import (
func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
}
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
}
// IsDockerEndpoint returns true if this is a docker endpoint
func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.DockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
}

View File

@@ -3,7 +3,7 @@ package jwt
import (
"errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"fmt"
"time"
@@ -51,13 +51,23 @@ func NewService(userSessionDuration string) (*Service, error) {
// GenerateToken generates a new JWT token.
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
return service.generateSignedToken(data, nil)
}
expireToken := time.Now().Add(service.userSessionTimeout).Unix()
cl := claims{
UserID: int(data.ID),
Username: data.Username,
Role: int(data.Role),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
// GenerateTokenForOAuth generates a new JWT for OAuth login
// token expiry time from the OAuth provider is considered
func (service *Service) GenerateTokenForOAuth(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
return service.generateSignedToken(data, expiryTime)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}
return signedToken, nil
}
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
@@ -87,26 +97,3 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration) {
service.userSessionTimeout = userSessionDuration
}
func (service *Service) generateSignedToken(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
expireToken := time.Now().Add(service.userSessionTimeout).Unix()
if expiryTime != nil && !expiryTime.IsZero() {
expireToken = expiryTime.Unix()
}
cl := claims{
UserID: int(data.ID),
Username: data.Username,
Role: int(data.Role),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}
return signedToken, nil
}

View File

@@ -1,38 +0,0 @@
package jwt
import (
"testing"
"time"
"github.com/dgrijalva/jwt-go"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestGenerateSignedToken(t *testing.T) {
svc, err := NewService("24h")
assert.NoError(t, err, "failed to create a copy of service")
token := &portainer.TokenData{
Username: "Joe",
ID: 1,
Role: 1,
}
expirtationTime := time.Now().Add(1 * time.Hour)
generatedToken, err := svc.generateSignedToken(token, &expirtationTime)
assert.NoError(t, err, "failed to generate a signed token")
parsedToken, err := jwt.ParseWithClaims(generatedToken, &claims{}, func(token *jwt.Token) (interface{}, error) {
return svc.secret, nil
})
assert.NoError(t, err, "failed to parse generated token")
tokenClaims, ok := parsedToken.Claims.(*claims)
assert.Equal(t, true, ok, "failed to claims out of generated ticket")
assert.Equal(t, token.Username, tokenClaims.Username)
assert.Equal(t, int(token.ID), tokenClaims.UserID)
assert.Equal(t, int(token.Role), tokenClaims.Role)
assert.Equal(t, expirtationTime.Unix(), tokenClaims.ExpiresAt)
}

View File

@@ -3,14 +3,18 @@ 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"
)
type (
namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
accessPolicies struct {
UserAccessPolicies portainer.UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies portainer.TeamAccessPolicies `json:"TeamAccessPolicies"`
}
namespaceAccessPolicies map[string]accessPolicies
)
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
@@ -65,7 +69,7 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
return nil
}
func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sNamespaceAccessPolicy) bool {
func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies) bool {
_, userAccess := policies.UserAccessPolicies[portainer.UserID(userID)]
if userAccess {
return true
@@ -80,65 +84,3 @@ 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) {
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
var policies map[string]portainer.K8sNamespaceAccessPolicy
err = json.Unmarshal([]byte(accessData), &policies)
if err != nil {
return nil, err
}
return policies, nil
}
// UpdateNamespaceAccessPolicies updates the namespace access policies
func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]portainer.K8sNamespaceAccessPolicy) error {
data, err := json.Marshal(accessPolicies)
if err != nil {
return err
}
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
configMap.Data[portainerConfigMapAccessPoliciesKey] = string(data)
_, err = kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Update(configMap)
if err != nil {
return err
}
return nil
}

View File

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

View File

@@ -14,18 +14,13 @@ 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 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 {
// 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 {
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

@@ -4,15 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"golang.org/x/oauth2"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
)
// Service represents a service used to authenticate users against an authorization server
@@ -24,35 +23,31 @@ 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
// On success, it will then return the username 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, error) {
token, err := getOAuthToken(code, configuration)
token, err := getAccessToken(code, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
return "", err
}
username, err := getUsername(token.AccessToken, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
return "", err
}
return username, nil
return getUsername(token, configuration)
}
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
func getAccessToken(code string, configuration *portainer.OAuthSettings) (string, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
return nil, err
return "", err
}
config := buildConfig(configuration)
token, err := config.Exchange(context.Background(), unescapedCode)
if err != nil {
return nil, err
return "", err
}
return token, nil
return token.AccessToken, nil
}
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {

View File

@@ -2,7 +2,6 @@ package portainer
import (
"io"
"net/http"
"time"
gittypes "github.com/portainer/portainer/api/git/types"
@@ -393,11 +392,6 @@ type (
// JobType represents a job type
JobType int
K8sNamespaceAccessPolicy struct {
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
}
// KubernetesData contains all the Kubernetes related endpoint information
KubernetesData struct {
Snapshots []KubernetesSnapshot `json:"Snapshots"`
@@ -497,8 +491,6 @@ type (
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
}
// Pair defines a key/value string pair
@@ -1156,7 +1148,6 @@ type (
// JWTService represents a service for managing JWT tokens
JWTService interface {
GenerateToken(data *TokenData) (string, error)
GenerateTokenForOAuth(data *TokenData, expiryTime *time.Time) (string, error)
ParseAndVerifyToken(token string) (*TokenData, error)
SetUserSessionDuration(userSessionDuration time.Duration)
}
@@ -1165,16 +1156,12 @@ type (
KubeClient interface {
SetupUserServiceAccount(userID int, teamIDs []int) error
GetServiceAccountBearerToken(userID int) (string, 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
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
}
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
KubernetesDeployer interface {
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
ConvertCompose(data string) ([]byte, error)
Deploy(endpoint *Endpoint, data string, composeFormat bool, namespace string) ([]byte, error)
}
// KubernetesSnapshotter represents a service used to create Kubernetes endpoint snapshots
@@ -1343,9 +1330,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.6.2"
APIVersion = "2.5.1"
// DBVersion is the version number of the Portainer database
DBVersion = 30
DBVersion = 27
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server

View File

@@ -20,18 +20,18 @@ export function ContainerGroupDefaultModel() {
}
export function ContainerGroupViewModel(data) {
const addressPorts = data.properties.ipAddress ? data.properties.ipAddress.ports : [];
const addressPorts = data.properties.ipAddress.ports;
const container = data.properties.containers.length ? data.properties.containers[0] : {};
const containerPorts = container ? container.properties.ports : [];
this.Id = data.id;
this.Name = data.name;
this.Location = data.location;
this.IPAddress = data.properties.ipAddress ? data.properties.ipAddress.ip : '';
this.IPAddress = data.properties.ipAddress.ip;
this.Ports = addressPorts.length ? addressPorts.map((binding, index) => ({ container: containerPorts[index].port, host: binding.port, protocol: binding.protocol })) : [];
this.Image = container.properties.image || '';
this.OSType = data.properties.osType;
this.AllocatePublicIP = data.properties.ipAddress && data.properties.ipAddress.type === 'Public';
this.AllocatePublicIP = data.properties.ipAddress.type === 'Public';
this.CPU = container.properties.resources.requests.cpu;
this.Memory = container.properties.resources.requests.memoryInGB;

View File

@@ -63,7 +63,7 @@
</div>
<!-- !os-input -->
<!-- port-mapping -->
<div class="form-group" ng-if="$ctrl.container.Ports.length > 0">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
</div>

View File

@@ -8,8 +8,7 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
'Notifications',
'Authentication',
'ResourceControlService',
'FormValidator',
function ($q, $scope, $state, AzureService, Notifications, Authentication, ResourceControlService, FormValidator) {
function ($q, $scope, $state, AzureService, Notifications, Authentication, ResourceControlService) {
var allResourceGroups = [];
var allProviders = [];
@@ -71,11 +70,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
return 'At least one port binding is required';
}
const error = FormValidator.validateAccessControl(model.AccessControlData, Authentication.isAdmin());
if (error !== '') {
return error;
}
return null;
}

View File

@@ -30,5 +30,3 @@ angular
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none'])
.constant('KUBERNETES_DEFAULT_NAMESPACE', 'default')
.constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']);
export const PORTAINER_FADEOUT = 1500;

View File

@@ -456,7 +456,7 @@ angular.module('portainer.docker', ['portainer.app']).config([
var stack = {
name: 'docker.stacks.stack',
url: '/:name?id&type&regular&external&orphaned&orphanedRunning',
url: '/:name?id&type&external',
views: {
'content@': {
templateUrl: '~Portainer/views/stacks/edit/stack.html',

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.limit || (this.pullRateLimits.limit && this.pullRateLimits.remaining >= 0));
this.setValidity(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.line, '')]);
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
};
},

View File

@@ -67,6 +67,39 @@ angular.module('portainer.docker').factory('ServiceHelper', [
return [];
};
helper.translateEnvironmentVariables = function (env) {
if (env) {
var variables = [];
env.forEach(function (variable) {
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)];
var originalValue = keyValue.length > 1 ? keyValue[1] : '';
variables.push({
key: keyValue[0],
value: originalValue,
originalKey: keyValue[0],
originalValue: originalValue,
added: true,
});
});
return variables;
}
return [];
};
helper.translateEnvironmentVariablesToEnv = function (env) {
if (env) {
var variables = [];
env.forEach(function (variable) {
if (variable.key && variable.key !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
};
helper.translatePreferencesToKeyValue = function (preferences) {
if (preferences) {
var keyValuePreferences = [];

View File

@@ -91,20 +91,6 @@ export function ContainerStatsViewModel(data) {
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
}
this.Networks = _.values(data.networks);
if (data.blkio_stats !== undefined) {
//TODO: take care of multiple block devices
var readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Read');
if (readData !== undefined) {
this.BytesRead = readData.value;
}
var writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Write');
if (writeData !== undefined) {
this.BytesWrite = writeData.value;
}
} else {
//no IO related data is available
this.noIOdata = true;
}
}
export function ContainerDetailsViewModel(data) {

View File

@@ -18,10 +18,6 @@ function isJSON(jsonString) {
// This handler wrap the JSON objects in an array.
// Used by the API in: Image push, Image create, Events query.
export function jsonObjectsToArrayHandler(data) {
// catching empty data helps the function not to fail and prevents unwanted error message to user.
if (!data) {
return [];
}
var str = '[' + data.replace(/\n/g, ' ').replace(/\}\s*\{/g, '}, {') + ']';
return angular.fromJson(str);
}

View File

@@ -1,8 +1,5 @@
import _ from 'lodash-es';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '../../../models/container';
@@ -81,7 +78,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
MemoryReservation: 0,
CmdMode: 'default',
EntrypointMode: 'default',
Env: [],
NodeName: null,
capabilities: [],
Sysctls: [],
@@ -99,11 +95,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
pullImageValidity: true,
};
$scope.handleEnvVarChange = handleEnvVarChange;
function handleEnvVarChange(value) {
$scope.formValues.Env = value;
}
$scope.refreshSlider = function () {
$timeout(function () {
$scope.$broadcast('rzSliderForceRender');
@@ -162,6 +153,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.formValues.Volumes.splice(index, 1);
};
$scope.addEnvironmentVariable = function () {
$scope.config.Env.push({ name: '', value: '' });
};
$scope.removeEnvironmentVariable = function (index) {
$scope.config.Env.splice(index, 1);
};
$scope.addPortBinding = function () {
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
@@ -255,7 +254,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
function prepareEnvironmentVariables(config) {
config.Env = envVarsUtils.convertToArrayOfStrings($scope.formValues.Env);
var env = [];
config.Env.forEach(function (v) {
if (v.name && v.value) {
env.push(v.name + '=' + v.value);
}
});
config.Env = env;
}
function prepareVolumes(config) {
@@ -532,7 +537,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
function loadFromContainerEnvironmentVariables() {
$scope.formValues.Env = envVarsUtils.parseArrayOfStrings($scope.config.Env);
var envArr = [];
for (var e in $scope.config.Env) {
if ({}.hasOwnProperty.call($scope.config.Env, e)) {
var arr = $scope.config.Env[e].split(/\=(.*)/);
envArr.push({ name: arr[0], value: arr[1] });
}
}
$scope.config.Env = envArr;
}
function loadFromContainerLabels() {

View File

@@ -583,13 +583,37 @@
<!-- !tab-labels -->
<!-- tab-env -->
<div class="tab-pane" id="env">
<environment-variables-panel
ng-model="formValues.Env"
explanation="These values will be applied to the container when deployed"
on-change="(handleEnvVarChange)"
></environment-variables-panel>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- environment-variables -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-env -->
<!-- !tab-labels -->
<!-- tab-restart-policy -->
<div class="tab-pane" id="restart-policy">
<form class="form-horizontal" style="margin-top: 15px;">

View File

@@ -14,7 +14,6 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
$scope.state = {
refreshRate: '5',
networkStatsUnavailable: false,
ioStatsUnavailable: false,
};
$scope.$on('$destroy', function () {
@@ -45,13 +44,6 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart);
}
function updateIOChart(stats, chart) {
var label = moment(stats.read).format('HH:mm:ss');
if (stats.noIOData !== true) {
ChartService.UpdateIOChart(label, stats.BytesRead, stats.BytesWrite, chart);
}
}
function updateCPUChart(stats, chart) {
var label = moment(stats.read).format('HH:mm:ss');
var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats);
@@ -85,15 +77,14 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
var networkChart = $scope.networkChart;
var cpuChart = $scope.cpuChart;
var memoryChart = $scope.memoryChart;
var ioChart = $scope.ioChart;
stopRepeater();
setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart);
setUpdateRepeater(networkChart, cpuChart, memoryChart);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(1500);
};
function startChartUpdate(networkChart, cpuChart, memoryChart, ioChart) {
function startChartUpdate(networkChart, cpuChart, memoryChart) {
$q.all({
stats: ContainerService.containerStats($transition$.params().id),
top: ContainerService.containerTop($transition$.params().id),
@@ -104,14 +95,10 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
if (stats.Networks.length === 0) {
$scope.state.networkStatsUnavailable = true;
}
if (stats.noIOData === true) {
$scope.state.ioStatsUnavailable = true;
}
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
updateIOChart(stats, ioChart);
setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart);
setUpdateRepeater(networkChart, cpuChart, memoryChart);
})
.catch(function error(err) {
stopRepeater();
@@ -119,7 +106,7 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
});
}
function setUpdateRepeater(networkChart, cpuChart, memoryChart, ioChart) {
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
var refreshRate = $scope.state.refreshRate;
$scope.repeater = $interval(function () {
$q.all({
@@ -132,7 +119,6 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
updateIOChart(stats, ioChart);
})
.catch(function error(err) {
stopRepeater();
@@ -154,11 +140,7 @@ angular.module('portainer.docker').controller('ContainerStatsController', [
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
$scope.memoryChart = memoryChart;
var ioChartCtx = $('#ioChart');
var ioChart = ChartService.CreateIOChart(ioChartCtx);
$scope.ioChart = ioChart;
startChartUpdate(networkChart, cpuChart, memoryChart, ioChart);
startChartUpdate(networkChart, cpuChart, memoryChart);
}
function initView() {

View File

@@ -42,11 +42,6 @@
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> Network stats are unavailable for this container. </span>
</div>
</div>
<div class="form-group" ng-if="state.ioStatsUnavailable">
<div class="col-sm-12">
<span class="small text-muted"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> I/O stats are unavailable for this container. </span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
@@ -54,7 +49,7 @@
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-12">
<div ng-class="{ true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12' }[state.networkStatsUnavailable]">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="Memory usage"></rd-widget-header>
<rd-widget-body>
@@ -64,8 +59,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-6 col-sm-12">
<div ng-class="{ true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12' }[state.networkStatsUnavailable]">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="CPU usage"></rd-widget-header>
<rd-widget-body>
@@ -75,8 +69,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-6 col-sm-12" ng-if="!state.networkStatsUnavailable">
<div class="col-lg-4 col-md-12 col-sm-12" ng-if="!state.networkStatsUnavailable">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="Network usage (aggregate)"></rd-widget-header>
<rd-widget-body>
@@ -87,19 +80,6 @@
</rd-widget>
</div>
<div class="col-lg-6 col-md-6 col-sm-12" ng-if="!state.ioStatsUnavailable">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="I/O usage (aggregate)"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="ioChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<container-processes-datatable
title-text="Processes"

View File

@@ -1,56 +1,58 @@
angular.module('portainer.docker').controller('BuildImageController', BuildImageController);
angular.module('portainer.docker').controller('BuildImageController', [
'$scope',
'$window',
'ModalService',
'BuildService',
'Notifications',
'HttpRequestHelper',
function ($scope, $window, ModalService, BuildService, Notifications, HttpRequestHelper) {
$scope.state = {
BuildType: 'editor',
actionInProgress: false,
activeTab: 0,
isEditorDirty: false,
};
function BuildImageController($scope, $async, $window, ModalService, BuildService, Notifications, HttpRequestHelper) {
$scope.state = {
BuildType: 'editor',
actionInProgress: false,
activeTab: 0,
isEditorDirty: false,
};
$scope.formValues = {
ImageNames: [{ Name: '' }],
UploadFile: null,
DockerFileContent: '',
URL: '',
Path: 'Dockerfile',
NodeName: null,
};
$scope.formValues = {
ImageNames: [{ Name: '' }],
UploadFile: null,
DockerFileContent: '',
URL: '',
Path: 'Dockerfile',
NodeName: null,
};
$window.onbeforeunload = () => {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$window.onbeforeunload = () => {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return '';
$scope.addImageName = function () {
$scope.formValues.ImageNames.push({ Name: '' });
};
$scope.removeImageName = function (index) {
$scope.formValues.ImageNames.splice(index, 1);
};
function buildImageBasedOnBuildType(method, names) {
var buildType = $scope.state.BuildType;
var dockerfilePath = $scope.formValues.Path;
if (buildType === 'upload') {
var file = $scope.formValues.UploadFile;
return BuildService.buildImageFromUpload(names, file, dockerfilePath);
} else if (buildType === 'url') {
var URL = $scope.formValues.URL;
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
} else {
var dockerfileContent = $scope.formValues.DockerFileContent;
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
}
}
};
$scope.addImageName = function () {
$scope.formValues.ImageNames.push({ Name: '' });
};
$scope.removeImageName = function (index) {
$scope.formValues.ImageNames.splice(index, 1);
};
function buildImageBasedOnBuildType(method, names) {
var buildType = $scope.state.BuildType;
var dockerfilePath = $scope.formValues.Path;
if (buildType === 'upload') {
var file = $scope.formValues.UploadFile;
return BuildService.buildImageFromUpload(names, file, dockerfilePath);
} else if (buildType === 'url') {
var URL = $scope.formValues.URL;
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
} else {
var dockerfileContent = $scope.formValues.DockerFileContent;
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
}
}
$scope.buildImage = buildImage;
async function buildImage() {
return $async(async () => {
$scope.buildImage = function () {
var buildType = $scope.state.BuildType;
if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') {
@@ -69,42 +71,44 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
try {
const data = await buildImageBasedOnBuildType(buildType, imageNames);
$scope.buildLogs = data.buildLogs;
$scope.state.activeTab = 1;
if (data.hasError) {
Notifications.error('An error occurred during build', { msg: 'Please check build logs output' });
} else {
Notifications.success('Image successfully built');
$scope.state.isEditorDirty = false;
buildImageBasedOnBuildType(buildType, imageNames)
.then(function success(data) {
$scope.buildLogs = data.buildLogs;
$scope.state.activeTab = 1;
if (data.hasError) {
Notifications.error('An error occured during build', { msg: 'Please check build logs output' });
} else {
Notifications.success('Image successfully built');
$scope.state.isEditorDirty = false;
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to build image');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.validImageNames = function () {
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
var item = $scope.formValues.ImageNames[i];
if (item.Name !== '') {
return true;
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to build image');
} finally {
$scope.state.actionInProgress = false;
}
});
}
return false;
};
$scope.validImageNames = function () {
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
var item = $scope.formValues.ImageNames[i];
if (item.Name !== '') {
return true;
$scope.editorUpdate = function (cm) {
$scope.formValues.DockerFileContent = cm.getValue();
$scope.state.isEditorDirty = true;
};
this.uiCanExit = async function () {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return ModalService.confirmWebEditorDiscard();
}
}
return false;
};
$scope.editorUpdate = function (cm) {
$scope.formValues.DockerFileContent = cm.getValue();
$scope.state.isEditorDirty = true;
};
this.uiCanExit = async function () {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return ModalService.confirmWebEditorDiscard();
}
};
}
};
},
]);

View File

@@ -25,7 +25,7 @@ angular.module('portainer.docker').controller('ImportImageController', [
Notifications.success('Images successfully uploaded');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to upload image');
Notifications.error('Failure', err.message, 'Unable to upload image');
})
.finally(function final() {
$scope.state.actionInProgress = false;

View File

@@ -1,6 +1,4 @@
import _ from 'lodash-es';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
@@ -111,11 +109,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope.allowBindMounts = false;
$scope.handleEnvVarChange = handleEnvVarChange;
function handleEnvVarChange(value) {
$scope.formValues.Env = value;
}
$scope.refreshSlider = function () {
$timeout(function () {
$scope.$broadcast('rzSliderForceRender');
@@ -175,6 +168,14 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope.formValues.Secrets.splice(index, 1);
};
$scope.addEnvironmentVariable = function () {
$scope.formValues.Env.push({ name: '', value: '' });
};
$scope.removeEnvironmentVariable = function (index) {
$scope.formValues.Env.splice(index, 1);
};
$scope.addPlacementConstraint = function () {
$scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' });
};
@@ -276,7 +277,13 @@ angular.module('portainer.docker').controller('CreateServiceController', [
}
function prepareEnvConfig(config, input) {
config.TaskTemplate.ContainerSpec.Env = envVarsUtils.convertToArrayOfStrings(input.Env);
var env = [];
input.Env.forEach(function (v) {
if (v.name) {
env.push(v.name + '=' + v.value);
}
});
config.TaskTemplate.ContainerSpec.Env = env;
}
function prepareLabelsConfig(config, input) {

View File

@@ -160,7 +160,6 @@
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command & Logging</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#env" data-toggle="tab">Env</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config & Restart</a></li>
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
@@ -203,6 +202,34 @@
</div>
</div>
<!-- !workdir-user-input -->
<!-- environment-variables -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
<div class="col-sm-12 form-section-title">
Logging
</div>
@@ -416,15 +443,6 @@
</form>
</div>
<!-- !tab-network -->
<!-- tab-env -->
<div class="tab-pane" id="env">
<environment-variables-panel
ng-model="formValues.Env"
explanation="These values will be applied to the service when created"
on-change="(handleEnvVarChange)"
></environment-variables-panel>
</div>
<!-- !tab-env -->
<!-- tab-labels -->
<div class="tab-pane" id="labels">
<form class="form-horizontal" style="margin-top: 15px;">

View File

@@ -1,8 +1,8 @@
<ng-form ng-if="service.EnvironmentVariables" id="service-env-variables" name="serviceEnvForm">
<div ng-if="service.EnvironmentVariables" id="service-env-variables">
<rd-widget>
<rd-widget-header icon="fa-tasks" title-text="Environment variables">
<div class="nopadding" authorization="DockerServiceUpdate">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addEnvironmentVariable(service)" ng-disabled="isUpdating">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addEnvironmentVariable(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</a>
</div>
@@ -10,20 +10,49 @@
<rd-widget-body ng-if="service.EnvironmentVariables.length === 0">
<p>There are no environment variables for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.EnvironmentVariables.length > 0">
<environment-variables-panel is-name-disabled="true" ng-model="service.EnvironmentVariables" on-change="(onChangeEnvVars)"></environment-variables-panel>
<rd-widget-body ng-if="service.EnvironmentVariables.length > 0" classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="var in service.EnvironmentVariables | orderBy: 'originalKey'">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added || isUpdating" placeholder="e.g. FOO" />
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input
type="text"
class="form-control"
ng-model="var.value"
ng-change="updateEnvironmentVariable(service, var)"
placeholder="e.g. bar"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
/>
<span class="input-group-btn" authorization="DockerServiceUpdate">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, var)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer authorization="DockerServiceUpdate">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!hasChanges(service, ['EnvironmentVariables']) || serviceEnvForm.$invalid"
ng-click="updateService(service)"
>
Apply changes
</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['EnvironmentVariables'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
@@ -35,4 +64,4 @@
</div>
</rd-widget-footer>
</rd-widget>
</ng-form>
</div>

View File

@@ -18,9 +18,6 @@ require('./includes/tasks.html');
require('./includes/updateconfig.html');
import _ from 'lodash-es';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ServiceController', [
@@ -117,25 +114,21 @@ angular.module('portainer.docker').controller('ServiceController', [
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ name: '', value: '' });
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
};
$scope.onChangeEnvVars = onChangeEnvVars;
function onChangeEnvVars(env) {
const service = $scope.service;
const orgEnv = service.EnvironmentVariables;
service.EnvironmentVariables = env.map((v) => {
const orgVar = orgEnv.find(({ name }) => v.name === name);
const added = orgVar && orgVar.added;
return { ...v, added };
});
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, item) {
const index = service.EnvironmentVariables.indexOf(item);
const removedElement = service.EnvironmentVariables.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
};
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
};
$scope.addConfig = function addConfig(service, config) {
if (
config &&
@@ -402,7 +395,7 @@ angular.module('portainer.docker').controller('ServiceController', [
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = envVarsUtils.convertToArrayOfStrings(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
if ($scope.hasChanges(service, ['Image'])) {
@@ -632,10 +625,7 @@ angular.module('portainer.docker').controller('ServiceController', [
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : [];
service.EnvironmentVariables = envVarsUtils
.parseArrayOfStrings(service.Env)
.map((v) => ({ ...v, added: true }))
.sort((v1, v2) => (v1.name > v2.name ? 1 : -1));
service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
service.LogDriverOpts = ServiceHelper.translateLogDriverOptsToKeyValue(service.LogDriverOpts);
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);

View File

@@ -182,16 +182,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const nodeStats = {
name: 'kubernetes.cluster.node.stats',
url: '/stats',
views: {
'content@': {
component: 'kubernetesNodeStatsView',
},
},
};
const dashboard = {
name: 'kubernetes.dashboard',
url: '/dashboard',
@@ -290,7 +280,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(deploy);
$stateRegistryProvider.register(node);
$stateRegistryProvider.register(nodeStats);
$stateRegistryProvider.register(resourcePools);
$stateRegistryProvider.register(resourcePoolCreation);
$stateRegistryProvider.register(resourcePool);

View File

@@ -107,9 +107,6 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAddress' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.useServerMetrics">
Actions
</th>
</tr>
</thead>
<tbody>
@@ -131,9 +128,6 @@
<td>{{ item.Memory | humansize }}</td>
<td>{{ item.Version }}</td>
<td>{{ item.IPAddress }}</td>
<td ng-if="$ctrl.useServerMetrics">
<a ui-sref="kubernetes.cluster.node.stats({ name: item.Name })" style="cursor: pointer;"> <i class="fa fa-chart-area" aria-hidden="true"></i> Stats </a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-center text-muted">Loading...</td>

View File

@@ -9,6 +9,5 @@ angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', {
orderBy: '@',
refreshCallback: '<',
isAdmin: '<',
useServerMetrics: '<',
},
});

View File

@@ -3,8 +3,7 @@
<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 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>
</div>
</div>

View File

@@ -261,7 +261,7 @@ class KubernetesApplicationConverter {
return res;
}
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels, ingresses) {
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) {
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, ingresses);
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts);
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, ingress) {
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
const generatePort = (port, rule) => {
const res = new KubernetesApplicationPublishedPortFormValue();
res.IsNew = false;
@@ -282,7 +282,6 @@ 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

@@ -57,9 +57,7 @@ export class KubernetesIngressConverter {
rule.IngressName = ingress.Name;
rule.ServiceName = serviceName;
rule.Port = p.ContainerPort;
if (p.IngressRoute) {
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
}
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
rule.Host = p.IngressHost;
ingress.Paths.push(rule);
}
@@ -173,7 +171,7 @@ export class KubernetesIngressConverter {
res.spec.rules = [];
_.forEach(data.Hosts, (host) => {
if (!host.NeedsDeletion) {
res.spec.rules.push({ host: host.Host || host });
res.spec.rules.push({ host: host.Host });
}
});
} else {

View File

@@ -10,7 +10,6 @@ class KubernetesMetricsService {
this.capabilitiesAsync = this.capabilitiesAsync.bind(this);
this.getPodAsync = this.getPodAsync.bind(this);
this.getNodeAsync = this.getNodeAsync.bind(this);
}
/**
@@ -28,26 +27,6 @@ class KubernetesMetricsService {
return this.$async(this.capabilitiesAsync, endpointID);
}
/**
* Stats of Node
*
* @param {string} nodeName
*/
async getNodeAsync(nodeName) {
try {
const params = new KubernetesCommonParams();
params.id = nodeName;
const data = await this.KubernetesMetrics().getNode(params).$promise;
return data;
} catch (err) {
throw new PortainerError('Unable to retrieve node stats', err);
}
}
getNode(nodeName) {
return this.$async(this.getNodeAsync, nodeName);
}
/**
* Stats
*

View File

@@ -20,10 +20,6 @@ angular.module('portainer.kubernetes').factory('KubernetesMetrics', [
method: 'GET',
url: podUrl,
},
getNode: {
method: 'GET',
url: `${url}/nodes/:id`,
},
}
);
};

View File

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

View File

@@ -2,13 +2,3 @@ export const KubernetesDeployManifestTypes = Object.freeze({
KUBERNETES: 1,
COMPOSE: 2,
});
export const KubernetesDeployBuildMethods = Object.freeze({
GIT: 1,
WEB_EDITOR: 2,
});
export const KubernetesDeployRequestMethods = Object.freeze({
REPOSITORY: 'repository',
STRING: 'string',
});

View File

@@ -1,9 +1,11 @@
export function KubernetesResourcePoolFormValues(defaults) {
this.Name = '';
this.MemoryLimit = defaults.MemoryLimit;
this.CpuLimit = defaults.CpuLimit;
this.HasQuota = false;
this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue
return {
Name: '',
MemoryLimit: defaults.MemoryLimit,
CpuLimit: defaults.CpuLimit,
HasQuota: false,
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
};
}
/**

View File

@@ -1,19 +0,0 @@
export const KubernetesResourceTypes = Object.freeze({
NAMESPACE: 'Namespace',
RESOURCEQUOTA: 'ResourceQuota',
CONFIGMAP: 'ConfigMap',
SECRET: 'Secret',
DEPLOYMENT: 'Deployment',
STATEFULSET: 'StatefulSet',
DAEMONSET: 'Daemonset',
PERSISTENT_VOLUME_CLAIM: 'PersistentVolumeClaim',
SERVICE: 'Service',
INGRESS: 'Ingress',
HORIZONTAL_POD_AUTOSCALER: 'HorizontalPodAutoscaler',
});
export const KubernetesResourceActions = Object.freeze({
CREATE: 'Create',
UPDATE: 'Update',
DELETE: 'Delete',
});

View File

@@ -220,11 +220,6 @@ class KubernetesApplicationService {
// resource creation flow
// should we keep formValues > Resource_1 || Resource_2
// or should we switch to formValues > Composite > Resource_1 || Resource_2
/**
* NOTE: Keep this method flow in sync with `getCreatedApplicationResources` method in the `applicationService` file
* To synchronise with kubernetes resource creation summary output, any new resources created in this method should
* also be displayed in the summary output (getCreatedApplicationResources)
*/
async createAsync(formValues) {
try {
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
@@ -271,11 +266,6 @@ class KubernetesApplicationService {
/* #region PATCH */
// this function accepts KubernetesApplicationFormValues as parameters
/**
* NOTE: Keep this method flow in sync with `getUpdatedApplicationResources` method in the `applicationService` file
* To synchronise with kubernetes resource creation, update and delete summary output, any new resources created
* in this method should also be displayed in the summary output (getUpdatedApplicationResources)
*/
async patchAsync(oldFormValues, newFormValues) {
try {
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);

View File

@@ -2,7 +2,7 @@
<span class="small">
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Advanced deployment allows you to deploy any Kubernetes manifest inside your cluster.
As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster.
</p>
<p>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy"> <i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment </button>

View File

@@ -5,7 +5,7 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div ng-if="ctrl.state.isAdmin" ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12">

View File

@@ -162,7 +162,7 @@
class="form-control"
ng-model="envVar.Name"
ng-change="ctrl.onChangeEnvironmentName()"
ng-pattern="/^[-._a-zA-Z][-._a-zA-Z0-9]*$/"
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
placeholder="foo"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
@@ -207,8 +207,8 @@
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must not
start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist alphanumeric characters, '-' or '_', start with an alphabetic
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
>
</ng-messages>
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
@@ -1368,7 +1368,7 @@
class="form-control"
name="ingress_hostname_{{ $index }}"
ng-model="publishedPort.IngressHost"
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in publishedPort.IngressHosts"
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in ctrl.ingressHostnames"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
>
@@ -1543,13 +1543,6 @@
</div>
<!-- #endregion -->
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view>
<div class="col-sm-12 form-section-title">
Actions
</div>

View File

@@ -321,7 +321,6 @@ 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;
}
@@ -389,7 +388,6 @@ 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();
@@ -482,7 +480,7 @@ class KubernetesCreateApplicationController {
const hasRWOOnly = KubernetesApplicationHelper.hasRWOOnly(this.formValues);
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
if (hasFolders && (hasRWOOnly || isIsolated)) {
if ((hasFolders && hasRWOOnly) || isIsolated) {
return false;
}
return true;
@@ -974,8 +972,7 @@ class KubernetesCreateApplicationController {
this.resourcePools,
this.configurations,
this.persistentVolumeClaims,
this.nodesLabels,
this.filteredIngresses
this.nodesLabels
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);

View File

@@ -89,7 +89,6 @@
order-by="Name"
refresh-callback="ctrl.getNodes"
is-admin="ctrl.isAdmin"
use-server-metrics="ctrl.state.useServerMetrics"
></kubernetes-nodes-datatable>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More