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
127 changed files with 713 additions and 3317 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(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(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(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,188 +2,71 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"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
binaryPath string
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(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,
binaryPath: binaryPath,
}
}
// 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(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
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", 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(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
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
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

@@ -5,7 +5,6 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -131,21 +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))
}
func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError {
token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err}
tokenData := &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
}
return response.JSON(w, &authenticateResponse{JWT: token})
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})
@@ -207,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

@@ -4,7 +4,6 @@ import (
"errors"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -26,24 +25,7 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) {
if code == "" {
return "", nil, errors.New("Invalid OAuth authorization code")
}
if settings == nil {
return "", nil, errors.New("Invalid OAuth configuration")
}
username, expiryTime, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", nil, err
}
return username, expiryTime, nil
}
// @id ValidateOAuth
// @id AuthenticateOauth
// @summary Authenticate with OAuth
// @tags auth
// @accept json
@@ -54,35 +36,52 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
// @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")
}
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
}
return username, nil
}
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, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized}
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 {
@@ -93,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 {
@@ -105,11 +104,11 @@ 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}
}
}
}
return handler.writeTokenForOAuth(w, user, expiryTime)
return handler.writeToken(w, user)
}

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

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

@@ -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}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(endpoint, payload.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 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(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(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(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

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

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

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

@@ -9,7 +9,12 @@ import (
)
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 {
@@ -64,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
@@ -79,50 +84,3 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
return false
}
// 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

@@ -4,16 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"time"
"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
@@ -25,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, *time.Time, error) {
token, err := getOAuthToken(code, configuration)
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
token, err := getAccessToken(code, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
return "", nil, err
return "", err
}
username, err := getUsername(token.AccessToken, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
return "", nil, err
}
return username, &token.Expiry, 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

@@ -392,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"`
@@ -496,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
@@ -1155,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,14 +1157,11 @@ type (
SetupUserServiceAccount(userID int, teamIDs []int) error
GetServiceAccountBearerToken(userID int) (string, error)
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
}
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
KubernetesDeployer interface {
Deploy(endpoint *Endpoint, data string, namespace string) (string, error)
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
@@ -1189,7 +1178,7 @@ type (
// OAuthService represents a service used to authenticate users using OAuth
OAuthService interface {
Authenticate(code string, configuration *OAuthSettings) (string, *time.Time, error)
Authenticate(code string, configuration *OAuthSettings) (string, error)
}
// RegistryService represents a service for managing registry data
@@ -1341,9 +1330,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.6.0"
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

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

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

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

@@ -480,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;

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>

View File

@@ -15,8 +15,7 @@ class KubernetesClusterController {
KubernetesNodeService,
KubernetesApplicationService,
KubernetesComponentStatusService,
KubernetesEndpointService,
EndpointProvider
KubernetesEndpointService
) {
this.$async = $async;
this.$state = $state;
@@ -27,7 +26,6 @@ class KubernetesClusterController {
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesComponentStatusService = KubernetesComponentStatusService;
this.KubernetesEndpointService = KubernetesEndpointService;
this.EndpointProvider = EndpointProvider;
this.onInit = this.onInit.bind(this);
this.getNodes = this.getNodes.bind(this);
@@ -134,7 +132,6 @@ class KubernetesClusterController {
}
this.state.viewReady = true;
this.state.useServerMetrics = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.UseServerMetrics;
}
$onInit() {

View File

@@ -1,71 +0,0 @@
<kubernetes-view-header title="Node stats" state="kubernetes.cluster.node.stats" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.cluster">Cluster</a> &gt; <a ui-sref="kubernetes.cluster.node({name: ctrl.state.transition.nodeName})"> {{ ctrl.state.transition.nodeName }} </a> >
{{ ctrl.state.transition.nodeName }}
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve node metrics">
<span class="small text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer was unable to retrieve any metrics associated to that node. Please contact your administrator to ensure that the Kubernetes metrics feature is properly configured.
</span>
</information-panel>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title-text="About statistics"> </rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
This view displays real-time statistics about the node <b>{{ ctrl.state.transition.nodeName }}</b
>.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
Refresh rate
</label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-show="ctrl.state.getMetrics">
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="Memory usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-node" style="position: relative;">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-chart-area" title-text="CPU usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-node" style="position: relative;">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesNodeStatsView', {
templateUrl: './stats.html',
controller: 'KubernetesNodeStatsController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
},
});

View File

@@ -1,144 +0,0 @@
import angular from 'angular';
import moment from 'moment';
import filesizeParser from 'filesize-parser';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { PORTAINER_FADEOUT } from '@/constants';
class KubernetesNodeStatsController {
/* @ngInject */
constructor($async, $state, $interval, $document, Notifications, KubernetesNodeService, KubernetesMetricsService, ChartService) {
this.$async = $async;
this.$state = $state;
this.$interval = $interval;
this.$document = $document;
this.Notifications = Notifications;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesMetricsService = KubernetesMetricsService;
this.ChartService = ChartService;
this.onInit = this.onInit.bind(this);
}
changeUpdateRepeater() {
var cpuChart = this.cpuChart;
var memoryChart = this.memoryChart;
this.stopRepeater();
this.setUpdateRepeater(cpuChart, memoryChart);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(PORTAINER_FADEOUT);
}
updateCPUChart() {
const label = moment(this.stats.read).format('HH:mm:ss');
this.ChartService.UpdateCPUChart(label, this.stats.CPUUsage, this.cpuChart);
}
updateMemoryChart() {
const label = moment(this.stats.read).format('HH:mm:ss');
this.ChartService.UpdateMemoryChart(label, this.stats.MemoryUsage, 0, this.memoryChart);
}
stopRepeater() {
var repeater = this.repeater;
if (angular.isDefined(repeater)) {
this.$interval.cancel(repeater);
this.repeater = undefined;
}
}
setUpdateRepeater() {
const refreshRate = this.state.refreshRate;
this.repeater = this.$interval(async () => {
try {
await this.getStats();
this.updateCPUChart();
this.updateMemoryChart();
} catch (error) {
this.stopRepeater();
this.Notifications.error('Failure', error);
}
}, refreshRate * 1000);
}
initCharts() {
const cpuChartCtx = $('#cpuChart');
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
this.cpuChart = cpuChart;
const memoryChartCtx = $('#memoryChart');
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
this.memoryChart = memoryChart;
this.updateCPUChart();
this.updateMemoryChart();
this.setUpdateRepeater();
}
getStats() {
return this.$async(async () => {
try {
const stats = await this.KubernetesMetricsService.getNode(this.state.transition.nodeName);
if (stats) {
const memory = filesizeParser(stats.usage.memory);
const cpu = KubernetesResourceReservationHelper.parseCPU(stats.usage.cpu);
this.stats = {
read: stats.creationTimestamp,
MemoryUsage: memory,
CPUUsage: (cpu / this.nodeCPU) * 100,
};
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve node stats');
}
});
}
$onDestroy() {
this.stopRepeater();
}
async onInit() {
this.state = {
autoRefresh: false,
refreshRate: '30',
viewReady: false,
transition: {
nodeName: this.$transition$.params().name,
},
getMetrics: true,
};
try {
const nodeMetrics = await this.KubernetesMetricsService.getNode(this.state.transition.nodeName);
if (nodeMetrics) {
const node = await this.KubernetesNodeService.get(this.state.transition.nodeName);
this.nodeCPU = node.CPU || 1;
await this.getStats();
if (this.state.getMetrics) {
this.$document.ready(() => {
this.initCharts();
});
}
} else {
this.state.getMetrics = false;
}
} catch (err) {
this.state.getMetrics = false;
this.Notifications.error('Failure', err, 'Unable to retrieve node stats');
} finally {
this.state.viewReady = true;
}
}
$onInit() {
return this.$async(this.onInit);
}
}
export default KubernetesNodeStatsController;
angular.module('portainer.kubernetes').controller('KubernetesNodeStatsController', KubernetesNodeStatsController);

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

@@ -123,12 +123,6 @@
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
Actions

View File

@@ -85,12 +85,6 @@
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)"
form-values="ctrl.formValues"
></kubernetes-summary-view>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
Actions

View File

@@ -141,32 +141,20 @@ class KubernetesConfigureController {
}
async removeIngressesAcrossNamespaces() {
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
if (!ingressesToDel.length) {
return;
}
const promises = [];
const oldEndpointID = this.EndpointProvider.endpointID();
this.EndpointProvider.setEndpointID(this.endpoint.Id);
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
const allResourcePools = await this.KubernetesResourcePoolService.get();
const resourcePools = _.filter(
allResourcePools,
(resourcePool) =>
!this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
);
try {
const allResourcePools = await this.KubernetesResourcePoolService.get();
const resourcePools = _.filter(
allResourcePools,
(resourcePool) =>
!this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
);
ingressesToDel.forEach((ingress) => {
resourcePools.forEach((resourcePool) => {
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
});
ingressesToDel.forEach((ingress) => {
resourcePools.forEach((resourcePool) => {
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
});
} finally {
this.EndpointProvider.setEndpointID(oldEndpointID);
}
});
const responses = await Promise.allSettled(promises);
responses.forEach((respons) => {
@@ -175,7 +163,7 @@ class KubernetesConfigureController {
}
});
}
enableMetricsServer() {
if (this.formValues.UseServerMetrics) {
this.state.metrics.userClick = true;

View File

@@ -52,162 +52,41 @@
</div>
</div>
<!-- !deploy-type -->
<!-- build method -->
<div class="col-sm-12 form-section-title">
Build method
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="build_method_git" ng-model="ctrl.state.BuildMethod" ng-value="ctrl.BuildMethods.GIT" />
<label for="build_method_git">
<div class="boxselector_header">
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
Git Repository
</div>
<p>Use a git repository</p>
</label>
</div>
<div>
<input type="radio" id="build_method_web_editor" ng-model="ctrl.state.BuildMethod" ng-value="ctrl.BuildMethods.WEB_EDITOR" />
<label for="build_method_web_editor">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
</div>
</div>
<!-- !deploy-type -->
<!-- repository -->
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
<div class="col-sm-12 form-section-title">
Git repository
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can use the URL of a git repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository URL</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.RepositoryURL"
id="stack_repository_url"
placeholder="https://github.com/portainer/deployment-repository"
/>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Specify a reference of the repository using the following syntax: branches with
<code>refs/heads/branch_name</code> or tags with <code>refs/tags/tag_name</code>. If not specified, will use the default <code>HEAD</code> reference normally
the <code>master</code> branch.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository reference</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.RepositoryReferenceName"
id="stack_repository_reference_name"
placeholder="refs/heads/master"
/>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the path to the yaml file from the root of your repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Manifest path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="ctrl.formValues.FilePathInRepository" id="stack_manifest_path" placeholder="deployment.yml" />
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Authentication
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RepositoryAuthentication" /><i></i> </label>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.RepositoryAuthentication">
<span class="col-sm-12 text-muted small">
If your git account has 2FA enabled, you may receive an
<code>authentication required</code> error when deploying your stack. In this case, you will need to provide a personal-access token instead of your password.
</span>
</div>
<div class="form-group" ng-if="ctrl.formValues.RepositoryAuthentication">
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
<div class="col-sm-11 col-md-5">
<input type="text" class="form-control" ng-model="ctrl.formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser" />
</div>
<label for="repository_password" class="col-sm-1 control-label text-left">
Password
</label>
<div class="col-sm-11 col-md-5">
<input type="password" class="form-control" ng-model="ctrl.formValues.RepositoryPassword" name="repository_password" placeholder="myPassword" />
</div>
</div>
</div>
<!-- !repository -->
<!-- editor -->
<div ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that
not all the Compose format options are supported by Kompose at the moment.
</p>
<p>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</p>
</span>
<span
class="col-sm-12 text-muted small"
ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES && ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR"
>
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here"
yml="false"
value="ctrl.formValues.EditorContent"
on-change="(ctrl.editorUpdate)"
></code-editor>
</div>
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not
all the Compose format options are supported by Kompose at the moment.
</p>
<p>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</p>
</span>
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here"
yml="false"
on-change="(ctrl.editorUpdate)"
></code-editor>
</div>
</div>
<!-- !editor -->

View File

@@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash-es';
import stripAnsi from 'strip-ansi';
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
import { KubernetesDeployManifestTypes } from 'Kubernetes/models/deploy';
class KubernetesDeployController {
/* @ngInject */
@@ -23,14 +23,7 @@ class KubernetesDeployController {
}
disableDeploy() {
const isGitFormInvalid =
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
(!this.formValues.RepositoryURL ||
!this.formValues.FilePathInRepository ||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword)));
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
return _.isEmpty(this.formValues.EditorContent) || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
}
async editorUpdateAsync(cm) {
@@ -53,28 +46,8 @@ class KubernetesDeployController {
this.state.actionInProgress = true;
try {
const method = this.state.BuildMethod === this.BuildMethods.GIT ? KubernetesDeployRequestMethods.REPOSITORY : KubernetesDeployRequestMethods.STRING;
const payload = {
ComposeFormat: this.state.DeployType === this.ManifestDeployTypes.COMPOSE,
Namespace: this.formValues.Namespace,
};
if (method === KubernetesDeployRequestMethods.REPOSITORY) {
payload.RepositoryURL = this.formValues.RepositoryURL;
payload.RepositoryReferenceName = this.formValues.RepositoryReferenceName;
payload.RepositoryAuthentication = this.formValues.RepositoryAuthentication ? true : false;
if (payload.RepositoryAuthentication) {
payload.RepositoryUsername = this.formValues.RepositoryUsername;
payload.RepositoryPassword = this.formValues.RepositoryPassword;
}
payload.FilePathInRepository = this.formValues.FilePathInRepository;
} else {
payload.StackFileContent = this.formValues.EditorContent;
}
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);
const compose = this.state.DeployType === this.ManifestDeployTypes.COMPOSE;
await this.StackService.kubernetesDeploy(this.endpointId, this.formValues.Namespace, this.formValues.EditorContent, compose);
this.Notifications.success('Manifest successfully deployed');
this.state.isEditorDirty = false;
this.$state.go('kubernetes.applications');
@@ -119,10 +92,10 @@ class KubernetesDeployController {
return this.ModalService.confirmWebEditorDiscard();
}
}
async onInit() {
this.state = {
DeployType: KubernetesDeployManifestTypes.KUBERNETES,
BuildMethod: KubernetesDeployBuildMethods.GIT,
tabLogsDisabled: true,
activeTab: 0,
viewReady: false,
@@ -131,7 +104,6 @@ class KubernetesDeployController {
this.formValues = {};
this.ManifestDeployTypes = KubernetesDeployManifestTypes;
this.BuildMethods = KubernetesDeployBuildMethods;
this.endpointId = this.EndpointProvider.endpointID();
await this.getNamespaces();

View File

@@ -338,10 +338,6 @@
<!-- #endregion -->
</div>
<!-- summary -->
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !ctrl.isCreateButtonDisabled()" form-values="ctrl.formValues"></kubernetes-summary-view>
<!-- !summary -->
<div class="col-sm-12 form-section-title">
Actions
</div>

View File

@@ -329,14 +329,6 @@
</div>
<!-- #endregion -->
<!-- summary -->
<kubernetes-summary-view
ng-if="resourcePoolEditForm.$valid && !ctrl.isUpdateButtonDisabled()"
form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view>
<!-- !summary -->
<!-- actions -->
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
Actions

View File

@@ -1,242 +0,0 @@
import _ from 'lodash-es';
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import {
KubernetesApplication,
KubernetesApplicationDeploymentTypes,
KubernetesApplicationPublishingTypes,
KubernetesApplicationTypes,
} from 'Kubernetes/models/application/models';
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
import KubernetesServiceConverter from 'Kubernetes/converters/service';
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
const { CREATE, UPDATE, DELETE } = KubernetesResourceActions;
/**
* Get summary of Kubernetes resources to be created, updated or deleted
* @param {KubernetesApplicationFormValues} formValues
*/
export default function (formValues, oldFormValues = {}) {
if (oldFormValues instanceof KubernetesApplicationFormValues) {
const resourceSummary = getUpdatedApplicationResources(oldFormValues, formValues);
return resourceSummary;
}
const resourceSummary = getCreatedApplicationResources(formValues);
return resourceSummary;
}
/**
* Get summary of Kubernetes resources to be created
* @param {KubernetesApplicationFormValues} formValues
*/
function getCreatedApplicationResources(formValues) {
const resources = [];
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
if (service) {
// Service
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: service.Name, type: service.Type || KubernetesServiceTypes.CLUSTER_IP });
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
// Ingress
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name);
resources.push(...getIngressUpdateSummary(formValues.OriginalIngresses, ingresses));
}
}
if (app instanceof KubernetesStatefulSet) {
// Service
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: headlessService.Name, type: headlessService.Type || KubernetesServiceTypes.CLUSTER_IP });
} else {
// Persistent volume claims
const persistentVolumeClaimResources = claims
.filter((pvc) => !pvc.PreviousName && !pvc.Id)
.map((pvc) => ({ action: CREATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: pvc.Name }));
resources.push(...persistentVolumeClaimResources);
}
// Horizontal pod autoscalers
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: autoScaler.Name });
}
// Deployment
const appResourceType = getApplicationResourceType(app);
if (appResourceType !== null) {
resources.push({ action: CREATE, kind: appResourceType, name: app.Name });
}
return resources;
}
/**
* Get summary of Kubernetes resources to be created, updated and/or deleted
* @param {KubernetesApplicationFormValues} oldFormValues
* @param {KubernetesApplicationFormValues} newFormValues
*/
function getUpdatedApplicationResources(oldFormValues, newFormValues) {
const resources = [];
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
const oldAppResourceType = getApplicationResourceType(oldApp);
const newAppResourceType = getApplicationResourceType(newApp);
if (oldAppResourceType !== newAppResourceType) {
// Deployment
resources.push({ action: DELETE, kind: oldAppResourceType, name: oldApp.Name });
if (oldService) {
// Service
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
}
// re-creation of resources
const createdApplicationResourceSummary = getCreatedApplicationResources(newFormValues);
resources.push(...createdApplicationResourceSummary);
return resources;
}
if (newApp instanceof KubernetesStatefulSet) {
const headlessServiceUpdateResourceSummary = getServiceUpdateResourceSummary(oldHeadlessService, newHeadlessService);
if (headlessServiceUpdateResourceSummary) {
resources.push(headlessServiceUpdateResourceSummary);
}
} else {
// Persistent volume claims
const claimSummaries = newClaims
.map((pvc) => {
if (!pvc.PreviousName && !pvc.Id) {
return { action: CREATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: pvc.Name };
} else if (!pvc.Id) {
const oldClaim = _.find(oldClaims, { Name: pvc.PreviousName });
return getVolumeClaimUpdateResourceSummary(oldClaim, pvc);
}
})
.filter((pvc) => pvc); // remove nulls
resources.push(...claimSummaries);
}
// Deployment
resources.push({ action: UPDATE, kind: oldAppResourceType, name: oldApp.Name });
if (oldService && newService) {
// Service
const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldService, newService);
if (serviceUpdateResourceSummary) {
resources.push(serviceUpdateResourceSummary);
}
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
// Ingress
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
resources.push(...getIngressUpdateSummary(oldIngresses, newIngresses));
}
} else if (!oldService && newService) {
// Service
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: newService.Name, type: newService.Type || KubernetesServiceTypes.CLUSTER_IP });
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
// Ingress
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
resources.push(...getIngressUpdateSummary(newFormValues.OriginalIngresses, ingresses));
}
} else if (oldService && !newService) {
// Service
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
// Ingress
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
resources.push(...getIngressUpdateSummary(oldFormValues.OriginalIngresses, ingresses));
}
}
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
if (!oldFormValues.AutoScaler.IsUsed) {
if (newFormValues.AutoScaler.IsUsed) {
// Horizontal pod autoscalers
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: newAutoScaler.Name });
}
} else {
// Horizontal pod autoscalers
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
if (newFormValues.AutoScaler.IsUsed) {
const hpaUpdateSummary = getHorizontalPodAutoScalerUpdateResourceSummary(oldAutoScaler, newAutoScaler);
if (hpaUpdateSummary) {
resources.push(hpaUpdateSummary);
}
} else {
resources.push({ action: DELETE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: oldAutoScaler.Name });
}
}
return resources;
}
function getApplicationResourceType(app) {
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
return KubernetesResourceTypes.DEPLOYMENT;
} else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) {
return KubernetesResourceTypes.DAEMONSET;
} else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) {
return KubernetesResourceTypes.STATEFULSET;
}
return null;
}
function getIngressUpdateSummary(oldIngresses, newIngresses) {
const ingressesSummaries = newIngresses
.map((newIng) => {
const oldIng = _.find(oldIngresses, { Name: newIng.Name });
return getIngressUpdateResourceSummary(oldIng, newIng);
})
.filter((s) => s); // remove nulls
return ingressesSummaries;
}
// getIngressUpdateResourceSummary replicates KubernetesIngressService.patch
function getIngressUpdateResourceSummary(oldIngress, newIngress) {
const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress);
if (payload.length) {
return { action: UPDATE, kind: KubernetesResourceTypes.INGRESS, name: oldIngress.Name };
}
return null;
}
// getVolumeClaimUpdateResourceSummary replicates KubernetesPersistentVolumeClaimService.patch
function getVolumeClaimUpdateResourceSummary(oldPVC, newPVC) {
const payload = KubernetesPersistentVolumeClaimConverter.patchPayload(oldPVC, newPVC);
if (payload.length) {
return { action: UPDATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: oldPVC.Name };
}
return null;
}
// getServiceUpdateResourceSummary replicates KubernetesServiceService.patch
function getServiceUpdateResourceSummary(oldService, newService) {
const payload = KubernetesServiceConverter.patchPayload(oldService, newService);
if (payload.length) {
return { action: UPDATE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP };
}
return null;
}
// getHorizontalPodAutoScalerUpdateResourceSummary replicates KubernetesHorizontalPodAutoScalerService.patch
function getHorizontalPodAutoScalerUpdateResourceSummary(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) {
const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler);
if (payload.length) {
return { action: UPDATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: oldHorizontalPodAutoScaler.Name };
}
return null;
}

View File

@@ -1,13 +0,0 @@
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
const { CREATE, UPDATE } = KubernetesResourceActions;
export default function (formValues) {
const action = formValues.Id ? UPDATE : CREATE;
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }];
} else if (formValues.Type === KubernetesConfigurationTypes.SECRET) {
return [{ action, kind: KubernetesResourceTypes.SECRET, name: formValues.Name }];
}
}

View File

@@ -1,54 +0,0 @@
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
function findCreateResources(newResources, oldResources) {
return _.differenceBy(newResources, oldResources, 'Name');
}
function findDeleteResources(newResources, oldResources) {
return _.differenceBy(oldResources, newResources, 'Name');
}
function findUpdateResources(newResources, oldResources) {
const updateResources = _.intersectionWith(newResources, oldResources, (newResource, oldResource) => {
// find out resources with same name but content changed
if (newResource.Name != oldResource.Name) {
return false;
}
return !isEqual(newResource, oldResource);
});
return updateResources;
}
function isEqual(newResource, oldResource) {
let patches = JsonPatch.compare(newResource, oldResource);
patches = _.filter(patches, (change) => {
return !_.includes(change.path, '$$hashKey') && !_.includes(change.path, 'Duplicate');
});
return !patches.length;
}
function doGetResourcesSummary(newResources, oldResources, kind, action, actionFilter) {
const filteredResources = actionFilter(newResources, oldResources);
const summary = filteredResources.map((resource) => ({ name: resource.Name, action, kind }));
return summary;
}
export function getResourcesSummary(newResources, oldResources, kind) {
if (!Array.isArray(newResources)) {
newResources = newResources ? [newResources] : [];
oldResources = oldResources ? [oldResources] : [];
}
const summary = [
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.CREATE, findCreateResources),
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.UPDATE, findUpdateResources),
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.DELETE, findDeleteResources),
];
return summary;
}

View File

@@ -1,23 +0,0 @@
import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool';
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
import { KubernetesResourceTypes } from 'Kubernetes/models/resource-types/models';
import { getResourcesSummary } from 'Kubernetes/views/summary/resources/helpers';
export default function (newFormValues, oldFormValues) {
const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
if (!(oldFormValues instanceof KubernetesResourcePoolFormValues)) {
oldFormValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
}
const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
const resources = [
...getResourcesSummary(newNamespace, oldNamespace, KubernetesResourceTypes.NAMESPACE),
...getResourcesSummary(newQuota, oldQuota, KubernetesResourceTypes.RESOURCEQUOTA),
...getResourcesSummary(newIngresses, oldIngresses, KubernetesResourceTypes.INGRESS),
];
return resources;
}

View File

@@ -1,38 +0,0 @@
<div
class="col-xs-12 form-section-title interactive"
ng-click="$ctrl.toggleSummary()"
style="display: flex; justify-content: space-between;"
ng-if="$ctrl.state.resources.length > 0"
>
Summary
<span class="small space-left">
<a ng-if="!$ctrl.state.expandedTemplate"><i class="fa fa-angle-down" aria-hidden="true"></i> expand</a>
<a ng-if="$ctrl.state.expandedTemplate"><i class="fa fa-angle-up" aria-hidden="true"></i> collapse</a>
</span>
</div>
<div class="form-group" ng-if="$ctrl.state.expandedTemplate">
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer will execute the following Kubernetes actions.
</div>
<div class="col-sm-12 small text-muted" style="padding-top: 1em;" ng-if="$ctrl.state.resources.length > 0">
<ul>
<li ng-repeat="summary in $ctrl.state.resources" ng-if="summary.action && summary.kind && summary.name">
{{ summary.action }}
{{ $ctrl.getArticle(summary.kind, summary.action) }}
<span style="color: black; font-weight: 700;">{{ summary.kind }}</span> named <code>{{ summary.name }}</code>
<span ng-if="summary.type">
of type <code>{{ summary.type }}</code></span
>
</li>
<li ng-if="$ctrl.state.limits.memory">
Set the memory resources limits and requests to <code>{{ $ctrl.state.limits.memory }}M</code>
</li>
<li ng-if="$ctrl.state.limits.cpu">
Set the CPU resources limits and requests to <code>{{ $ctrl.state.limits.cpu }}</code>
</li>
</ul>
</div>
</div>

View File

@@ -1,9 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesSummaryView', {
templateUrl: './summary.html',
controller: 'KubernetesSummaryController',
controllerAs: '$ctrl',
bindings: {
formValues: '<',
oldFormValues: '<',
},
});

View File

@@ -1,89 +0,0 @@
import angular from 'angular';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
import { KubernetesResourceActions, KubernetesResourceTypes } from 'Kubernetes/models/resource-types/models';
import getApplicationResources from './resources/applicationResources';
import getNamespaceResources from './resources/namespaceResources';
import getConfigurationResources from './resources/configurationResources';
class KubernetesSummaryController {
/* @ngInject */
constructor($scope, LocalStorage, KubernetesResourcePoolService) {
this.LocalStorage = LocalStorage;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.toggleSummary = this.toggleSummary.bind(this);
this.generateResourceSummaryList = this.generateResourceSummaryList.bind(this);
// Deep-watch changes on formValues property
$scope.$watch(
'$ctrl.formValues',
(formValues) => {
this.state.resources = this.generateResourceSummaryList(formValues);
},
true
);
}
getArticle(resourceType, resourceAction) {
let article = 'a';
if (resourceAction === KubernetesResourceActions.CREATE) {
if (resourceType === KubernetesResourceTypes.INGRESS) {
article = 'an';
}
} else {
article = 'the';
}
return article;
}
/**
* toggleSummary toggles the summary panel state and persists it to browser localstorage
*/
toggleSummary() {
this.state.expandedTemplate = !this.state.expandedTemplate;
this.LocalStorage.storeKubernetesSummaryToggle(this.state.expandedTemplate);
}
/**
* generateResourceSummaryList maps formValues to custom object
* @param {object} formValues
* @returns {object} => { action: "string", kind: "string", name: "string" }
*/
generateResourceSummaryList(formValues) {
const oldFormValues = this.oldFormValues;
if (formValues instanceof KubernetesConfigurationFormValues) {
// Configuration
return getConfigurationResources(formValues);
} else if (formValues instanceof KubernetesResourcePoolFormValues) {
// Namespaces
return getNamespaceResources(formValues, oldFormValues);
} else if (formValues instanceof KubernetesApplicationFormValues) {
// Applications
// extract cpu and memory requests & limits for pod
this.state.limits = { cpu: formValues.CpuLimit, memory: formValues.MemoryLimit };
return getApplicationResources(formValues, oldFormValues);
}
return [];
}
$onInit() {
const toggleValue = this.LocalStorage.getKubernetesSummaryToggle();
const expanded = typeof toggleValue === 'boolean' ? toggleValue : true;
this.state = {
expandedTemplate: expanded,
resources: [],
limits: { cpu: null, memory: null },
};
}
}
export default KubernetesSummaryController;
angular.module('portainer.kubernetes').controller('KubernetesSummaryController', KubernetesSummaryController);

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

@@ -8,6 +8,5 @@ angular.module('portainer.app').component('porAccessControlForm', {
// Optional. An existing resource control object that will be used to set
// the default values of the component.
resourceControl: '<',
hideTitle: '<',
},
});

View File

@@ -1,5 +1,5 @@
<div>
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title">
<div class="col-sm-12 form-section-title">
Access control
</div>
<!-- access-control-switch -->

View File

@@ -14,10 +14,6 @@
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_all_orphaned_stacks" type="checkbox" ng-model="$ctrl.settings.allOrphanedStacks" ng-change="$ctrl.onSettingsAllOrphanedStacksChange()" />
<label for="setting_all_orphaned_stacks">Show all orphaned stacks</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
@@ -155,26 +151,12 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a
ng-if="!$ctrl.offlineMode"
ui-sref="docker.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, regular: item.Regular, external: item.External, orphaned: item.Orphaned, orphanedRunning: item.OrphanedRunning })"
>{{ item.Name }}</a
>
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, external: item.External })">{{ item.Name }}</a>
<span ng-if="$ctrl.offlineMode">{{ item.Name }}</span>
<span ng-if="item.Regular && item.Status == 2" style="margin-left: 10px;" class="label label-warning image-tag space-left">Inactive</span>
<span ng-if="item.Status == 2" style="margin-left: 10px;" class="label label-warning image-tag space-left">Inactive</span>
</td>
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
<td>
<span
ng-if="item.Orphaned"
class="interactive"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="This stack was created inside an endpoint that is no longer registered inside Portainer."
>
Orphaned <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px;"></i>
</span>
<span
ng-if="item.External"
class="interactive"
@@ -185,7 +167,7 @@
>
Limited <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px;"></i>
</span>
<span ng-if="item.Regular">Total</span>
<span ng-if="!item.External">Total</span>
</td>
<td>
<span ng-if="item.CreationDate">{{ item.CreationDate | getisodatefromtimestamp }} {{ item.CreatedBy ? 'by ' + item.CreatedBy : '' }}</span>

View File

@@ -47,11 +47,7 @@ angular.module('portainer.app').controller('StacksDatatableController', [
this.applyFilters = applyFilters.bind(this);
function applyFilters(stack) {
const { showActiveStacks, showUnactiveStacks } = this.filters.state;
if (stack.Orphaned) {
return stack.OrphanedRunning || this.settings.allOrphanedStacks;
} else {
return (stack.Status === 1 && showActiveStacks) || (stack.Status === 2 && showUnactiveStacks) || stack.External || !stack.Status;
}
return (stack.Status === 1 && showActiveStacks) || (stack.Status === 2 && showUnactiveStacks) || stack.External;
}
this.onFilterChange = onFilterChange.bind(this);
@@ -61,10 +57,6 @@ angular.module('portainer.app').controller('StacksDatatableController', [
DatatableService.setDataTableFilters(this.tableKey, this.filters);
}
this.onSettingsAllOrphanedStacksChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
@@ -95,7 +87,6 @@ angular.module('portainer.app').controller('StacksDatatableController', [
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
this.settings.allOrphanedStacks = this.settings.allOrphanedStacks && this.isAdmin;
}
this.onSettingsRepeaterChange();

View File

@@ -1,34 +0,0 @@
import { parseDotEnvFile, convertToArrayOfStrings } from '@/portainer/helpers/env-vars';
export default class EnvironmentVariablesPanelController {
/* @ngInject */
constructor() {
this.mode = 'simple';
this.editorText = '';
this.switchEnvMode = this.switchEnvMode.bind(this);
this.editorUpdate = this.editorUpdate.bind(this);
this.handleSimpleChange = this.handleSimpleChange.bind(this);
}
switchEnvMode() {
if (this.mode === 'simple') {
const editorText = convertToArrayOfStrings(this.ngModel).join('\n');
this.editorText = editorText;
this.mode = 'advanced';
} else {
this.mode = 'simple';
}
}
handleSimpleChange(value) {
this.onChange(value);
}
editorUpdate(cm) {
this.editorText = cm.getValue();
this.onChange(parseDotEnvFile(this.editorText));
}
}

View File

@@ -1,11 +0,0 @@
.environment-variables-panel {
margin-top: 15px;
}
.environment-variables-panel--explanation {
margin-bottom: 5px;
}
.environment-variables-panel--advanced > * + * {
margin-top: 5px;
}

View File

@@ -1,30 +0,0 @@
<ng-form class="form-horizontal environment-variables-panel" name="$ctrl.envVarsForm">
<div class="form-group">
<div class="col-sm-12 form-section-title" style="margin-top: 10px; margin-left: 15px; width: 98%;">
Environment variables
</div>
<div class="col-sm-12 environment-variables-panel--explanation">
{{::$ctrl.explanation}}
</div>
<environment-variables-simple-mode
ng-if="$ctrl.mode == 'simple'"
ng-model="$ctrl.ngModel"
on-change="($ctrl.handleSimpleChange)"
on-switch-mode-click="($ctrl.switchEnvMode)"
></environment-variables-simple-mode>
<div ng-if="$ctrl.mode == 'advanced'" class="environment-variables-panel--advanced">
<div class="col-sm-12">
<a class="small interactive" ng-click="$ctrl.switchEnvMode()"> <i class="fa fa-list-ol space-right" aria-hidden="true"></i> Simple mode </a>
</div>
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon space-right" aria-hidden="true"></i>
Switch to simple mode to define variables line by line, or load from .env file
</div>
<div class="form-group" style="margin-left: 1px;">
<code-editor identifier="environment-variables-editor" placeholder="e.g. key=value" value="$ctrl.editorText" yml="false" on-change="($ctrl.editorUpdate)"></code-editor>
</div>
</div>
</div>
</ng-form>

View File

@@ -1,41 +0,0 @@
import { KEY_REGEX, VALUE_REGEX } from '@/portainer/helpers/env-vars';
class EnvironmentVariablesSimpleModeItemController {
/* @ngInject */
constructor() {
this.KEY_REGEX = KEY_REGEX;
this.VALUE_REGEX = VALUE_REGEX;
}
onChangeName(name) {
const fieldIsInvalid = typeof name === 'undefined';
if (fieldIsInvalid) {
return;
}
this.onChange(this.index, { ...this.variable, name });
}
onChangeValue(value) {
const fieldIsInvalid = typeof value === 'undefined';
if (fieldIsInvalid) {
return;
}
this.onChange(this.index, { ...this.variable, value });
}
hasValue() {
return typeof this.variable.value !== 'undefined';
}
removeValue() {
this.onChange(this.index, { name: this.variable.name });
}
$onInit() {
this.formName = `variableForm${this.index}`;
}
}
export default EnvironmentVariablesSimpleModeItemController;

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