mirror of https://github.com/portainer/portainer
Merge branch 'develop' into feat/EE-5028/security_teaser
commit
659402c3ec
|
@ -93,7 +93,7 @@ $ yarn start
|
||||||
|
|
||||||
Portainer can now be accessed at <https://localhost:9443>.
|
Portainer can now be accessed at <https://localhost:9443>.
|
||||||
|
|
||||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
|
||||||
|
|
||||||
### Build customisation
|
### Build customisation
|
||||||
|
|
||||||
|
@ -103,6 +103,10 @@ You can customise the following settings:
|
||||||
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
||||||
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
||||||
|
|
||||||
|
## Testing your build
|
||||||
|
|
||||||
|
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
|
||||||
|
|
||||||
## Adding api docs
|
## Adding api docs
|
||||||
|
|
||||||
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -42,7 +43,9 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||||
|
|
|
@ -72,6 +72,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +81,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
|
|
||||||
// ValidateFlags validates the values of the flags.
|
// ValidateFlags validates the values of the flags.
|
||||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
|
|
||||||
displayDeprecationWarnings(flags)
|
displayDeprecationWarnings(flags)
|
||||||
|
|
||||||
err := validateEndpointURL(*flags.EndpointURL)
|
err := validateEndpointURL(*flags.EndpointURL)
|
||||||
|
@ -111,31 +111,38 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointURL(endpointURL string) error {
|
func validateEndpointURL(endpointURL string) error {
|
||||||
if endpointURL != "" {
|
if endpointURL == "" {
|
||||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
return nil
|
||||||
return errInvalidEndpointProtocol
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
return errInvalidEndpointProtocol
|
||||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
}
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||||
return errSocketOrNamedPipeNotFound
|
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||||
}
|
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||||
return err
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return errSocketOrNamedPipeNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSnapshotInterval(snapshotInterval string) error {
|
func validateSnapshotInterval(snapshotInterval string) error {
|
||||||
if snapshotInterval != "" {
|
if snapshotInterval == "" {
|
||||||
_, err := time.ParseDuration(snapshotInterval)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return errInvalidSnapshotInterval
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := time.ParseDuration(snapshotInterval)
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidSnapshotInterval
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,14 @@ func Confirm(message string) (bool, error) {
|
||||||
fmt.Printf("%s [y/N]", message)
|
fmt.Printf("%s [y/N]", message)
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
answer, err := reader.ReadString('\n')
|
answer, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
answer = strings.Replace(answer, "\n", "", -1)
|
|
||||||
|
answer = strings.ReplaceAll(answer, "\n", "")
|
||||||
answer = strings.ToLower(answer)
|
answer = strings.ToLower(answer)
|
||||||
|
|
||||||
return answer == "y" || answer == "yes", nil
|
return answer == "y" || answer == "yes", nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -684,25 +684,21 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||||
}
|
}
|
||||||
|
|
||||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
|
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: In 2.16 we changed the way ingress controller permissions are
|
// Our normal migrations run as part of the database initialization
|
||||||
// stored. Instead of being stored as annotation on an ingress rule, we keep
|
// but some more complex migrations require access to a kubernetes or docker
|
||||||
// them in our database. However, in order to run the migration we need an
|
// client. Therefore we run a separate migration process just before
|
||||||
// admin kube client to run lookup the old ingress rules and compare them
|
// starting the server.
|
||||||
// with the current existing ingress classes.
|
postInitMigrator := datastore.NewPostInitMigrator(
|
||||||
//
|
kubernetesClientFactory,
|
||||||
// Unfortunately, our migrations run as part of the database initialization
|
dockerClientFactory,
|
||||||
// and our kubeclients require an initialized database. So it is not
|
dataStore,
|
||||||
// possible to do this migration as part of our normal flow. We DO have a
|
)
|
||||||
// migration which toggles a boolean in kubernetes configuration that
|
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||||
// indicated that this "post init" migration should be run. If/when this is
|
|
||||||
// resolved we can remove this function.
|
|
||||||
err = kubernetesClientFactory.PostInitMigrateIngresses()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/portainer/libcrypto"
|
"github.com/portainer/libcrypto"
|
||||||
)
|
)
|
||||||
|
@ -115,9 +114,6 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||||
|
|
||||||
hash := libcrypto.HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
|
|
||||||
r := big.NewInt(0)
|
|
||||||
s := big.NewInt(0)
|
|
||||||
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
||||||
var object string
|
var object string
|
||||||
err := conn.UnmarshalObject(test.object, &object)
|
err := conn.UnmarshalObject(test.object, &object)
|
||||||
is.NoError(err)
|
is.NoError(err)
|
||||||
is.Equal(test.expected, string(object))
|
is.Equal(test.expected, object)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, i
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bucket.Put(tx.conn.ConvertToKey(int(id)), data)
|
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||||
|
|
|
@ -9,8 +9,7 @@ import (
|
||||||
|
|
||||||
// NewDatabase should use config options to return a connection to the requested database
|
// NewDatabase should use config options to return a connection to the requested database
|
||||||
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
||||||
switch storeType {
|
if storeType == "boltdb" {
|
||||||
case "boltdb":
|
|
||||||
return &boltdb.DbConnection{
|
return &boltdb.DbConnection{
|
||||||
Path: storePath,
|
Path: storePath,
|
||||||
EncryptionKey: encryptionKey,
|
EncryptionKey: encryptionKey,
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostInitMigrator struct {
|
||||||
|
kubeFactory *cli.ClientFactory
|
||||||
|
dockerFactory *docker.ClientFactory
|
||||||
|
dataStore dataservices.DataStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *docker.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
|
||||||
|
return &PostInitMigrator{
|
||||||
|
kubeFactory: kubeFactory,
|
||||||
|
dockerFactory: dockerFactory,
|
||||||
|
dataStore: dataStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migrator *PostInitMigrator) PostInitMigrate() error {
|
||||||
|
if err := migrator.PostInitMigrateIngresses(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator.PostInitMigrateGPUs()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
|
||||||
|
endpoints, err := migrator.dataStore.Endpoint().Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range endpoints {
|
||||||
|
// Early exit if we do not need to migrate!
|
||||||
|
if !endpoints[i].PostInitMigrations.MigrateIngresses {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
|
||||||
|
// If there's an error getting the containers, we'll log it and move on
|
||||||
|
func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
|
||||||
|
environments, err := migrator.dataStore.Endpoint().Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failure getting endpoints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range environments {
|
||||||
|
if environments[i].Type == portainer.DockerEnvironment {
|
||||||
|
// // Early exit if we do not need to migrate!
|
||||||
|
if !environments[i].PostInitMigrations.MigrateGPUs {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the MigrateGPUs flag to false so we don't run this again
|
||||||
|
environments[i].PostInitMigrations.MigrateGPUs = false
|
||||||
|
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||||
|
|
||||||
|
// create a docker client
|
||||||
|
dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
|
||||||
|
// get all containers
|
||||||
|
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to list containers")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint
|
||||||
|
containersLoop:
|
||||||
|
for _, container := range containers {
|
||||||
|
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
|
||||||
|
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("failed to inspect container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||||
|
for _, deviceRequest := range deviceRequests {
|
||||||
|
if deviceRequest.Driver == "nvidia" {
|
||||||
|
environments[i].EnableGPUManagement = true
|
||||||
|
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||||
|
|
||||||
|
break containersLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,16 @@ package migrator
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Migrator) migrateDBVersionToDB90() error {
|
func (m *Migrator) migrateDBVersionToDB90() error {
|
||||||
if err := m.updateUserThemForDB90(); err != nil {
|
if err := m.updateUserThemeForDB90(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.updateEnableGpuManagementFeatures(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +44,7 @@ func (m *Migrator) updateEdgeStackStatusForDB90() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) updateUserThemForDB90() error {
|
func (m *Migrator) updateUserThemeForDB90() error {
|
||||||
log.Info().Msg("updating existing user theme settings")
|
log.Info().Msg("updating existing user theme settings")
|
||||||
|
|
||||||
users, err := m.userService.Users()
|
users, err := m.userService.Users()
|
||||||
|
@ -60,3 +65,28 @@ func (m *Migrator) updateUserThemForDB90() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) updateEnableGpuManagementFeatures() error {
|
||||||
|
// get all environments
|
||||||
|
environments, err := m.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, environment := range environments {
|
||||||
|
if environment.Type == portainer.DockerEnvironment {
|
||||||
|
// set the PostInitMigrations.MigrateGPUs to true on this environment to run the migration only on the 2.18 upgrade
|
||||||
|
environment.PostInitMigrations.MigrateGPUs = true
|
||||||
|
// if there's one or more gpu, set the EnableGpuManagement setting to true
|
||||||
|
gpuList := environment.Gpus
|
||||||
|
if len(gpuList) > 0 {
|
||||||
|
environment.EnableGPUManagement = true
|
||||||
|
}
|
||||||
|
// update the environment
|
||||||
|
if err := m.endpointService.UpdateEndpoint(environment.ID, &environment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
},
|
},
|
||||||
"EdgeCheckinInterval": 0,
|
"EdgeCheckinInterval": 0,
|
||||||
"EdgeKey": "",
|
"EdgeKey": "",
|
||||||
|
"EnableGPUManagement": false,
|
||||||
"Gpus": [],
|
"Gpus": [],
|
||||||
"GroupId": 1,
|
"GroupId": 1,
|
||||||
"Id": 1,
|
"Id": 1,
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
"UseServerMetrics": false
|
"UseServerMetrics": false
|
||||||
},
|
},
|
||||||
"Flags": {
|
"Flags": {
|
||||||
|
"IsServerIngressClassDetected": false,
|
||||||
"IsServerMetricsDetected": false,
|
"IsServerMetricsDetected": false,
|
||||||
"IsServerStorageDetected": false
|
"IsServerStorageDetected": false
|
||||||
},
|
},
|
||||||
|
@ -71,6 +73,7 @@
|
||||||
"LastCheckInDate": 0,
|
"LastCheckInDate": 0,
|
||||||
"Name": "local",
|
"Name": "local",
|
||||||
"PostInitMigrations": {
|
"PostInitMigrations": {
|
||||||
|
"MigrateGPUs": true,
|
||||||
"MigrateIngresses": true
|
"MigrateIngresses": true
|
||||||
},
|
},
|
||||||
"PublicURL": "",
|
"PublicURL": "",
|
||||||
|
@ -903,8 +906,7 @@
|
||||||
},
|
},
|
||||||
"Role": 1,
|
"Role": 1,
|
||||||
"ThemeSettings": {
|
"ThemeSettings": {
|
||||||
"color": "",
|
"color": ""
|
||||||
"subtleUpgradeButton": false
|
|
||||||
},
|
},
|
||||||
"TokenIssueAt": 0,
|
"TokenIssueAt": 0,
|
||||||
"UserTheme": "",
|
"UserTheme": "",
|
||||||
|
@ -934,8 +936,7 @@
|
||||||
},
|
},
|
||||||
"Role": 1,
|
"Role": 1,
|
||||||
"ThemeSettings": {
|
"ThemeSettings": {
|
||||||
"color": "",
|
"color": ""
|
||||||
"subtleUpgradeButton": false
|
|
||||||
},
|
},
|
||||||
"TokenIssueAt": 0,
|
"TokenIssueAt": 0,
|
||||||
"UserTheme": "",
|
"UserTheme": "",
|
||||||
|
@ -943,6 +944,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": {
|
"version": {
|
||||||
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -38,17 +38,19 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||||
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
|
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
|
||||||
// The underlying http client timeout may be specified, a default value is used otherwise.
|
// The underlying http client timeout may be specified, a default value is used otherwise.
|
||||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
switch endpoint.Type {
|
||||||
|
case portainer.AzureEnvironment:
|
||||||
return nil, errUnsupportedEnvironmentType
|
return nil, errUnsupportedEnvironmentType
|
||||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
case portainer.AgentOnDockerEnvironment:
|
||||||
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
|
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
|
||||||
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
case portainer.EdgeAgentOnDockerEnvironment:
|
||||||
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
|
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||||
return createLocalClient(endpoint)
|
return createLocalClient(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTCPClient(endpoint, timeout)
|
return createTCPClient(endpoint, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,9 +82,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||||
}
|
}
|
||||||
|
|
||||||
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
|
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
|
||||||
|
WorkingDir: stack.ProjectPath,
|
||||||
EnvFilePath: envFilePath,
|
EnvFilePath: envFilePath,
|
||||||
Host: url,
|
Host: url,
|
||||||
})
|
})
|
||||||
|
|
||||||
return errors.Wrap(err, "failed to remove a stack")
|
return errors.Wrap(err, "failed to remove a stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||||
|
@ -75,6 +76,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +86,9 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "logout")
|
args = append(args, "logout")
|
||||||
|
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +105,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "stack", "deploy", "--with-registry-auth")
|
args = append(args, "stack", "deploy", "--with-registry-auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pullImage {
|
if !pullImage {
|
||||||
args = append(args, "--resolve-image=never")
|
args = append(args, "--resolve-image=never")
|
||||||
}
|
}
|
||||||
|
@ -112,6 +117,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
||||||
for _, envvar := range stack.Env {
|
for _, envvar := range stack.Env {
|
||||||
env = append(env, envvar.Name+"="+envvar.Value)
|
env = append(env, envvar.Name+"="+envvar.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +127,9 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
|
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,6 +206,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
||||||
if config["HttpHeaders"] == nil {
|
if config["HttpHeaders"] == nil {
|
||||||
config["HttpHeaders"] = make(map[string]interface{})
|
config["HttpHeaders"] = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
headersObject := config["HttpHeaders"].(map[string]interface{})
|
headersObject := config["HttpHeaders"].(map[string]interface{})
|
||||||
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
|
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
|
||||||
headersObject["X-PortainerAgent-Signature"] = signature
|
headersObject["X-PortainerAgent-Signature"] = signature
|
||||||
|
@ -230,5 +239,6 @@ func configureFilePaths(args []string, filePaths []string) []string {
|
||||||
for _, path := range filePaths {
|
for _, path := range filePaths {
|
||||||
args = append(args, "--compose-file", path)
|
args = append(args, "--compose-file", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ func newHttpClientForAzure() *http.Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
||||||
|
|
||||||
return httpsCli
|
return httpsCli
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,10 +99,12 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, "failed to parse url")
|
return "", errors.WithMessage(err, "failed to parse url")
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
|
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, "failed to build download url")
|
return "", errors.WithMessage(err, "failed to build download url")
|
||||||
}
|
}
|
||||||
|
|
||||||
zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip")
|
zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, "failed to create temp file")
|
return "", errors.WithMessage(err, "failed to create temp file")
|
||||||
|
@ -133,6 +136,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
|
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return zipFile.Name(), nil
|
return zipFile.Name(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +145,7 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootItem.CommitId, nil
|
return rootItem.CommitId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +192,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
|
||||||
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
|
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
|
||||||
return nil, errors.Errorf("failed to get latest commitID in the repository")
|
return nil, errors.Errorf("failed to get latest commitID in the repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &items.Value[0], nil
|
return &items.Value[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +211,7 @@ func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||||
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
|
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
const expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
||||||
|
|
||||||
func parseSshUrl(rawUrl string) (*azureOptions, error) {
|
func parseSshUrl(rawUrl string) (*azureOptions, error) {
|
||||||
path := strings.Split(rawUrl, "/")
|
path := strings.Split(rawUrl, "/")
|
||||||
|
@ -343,6 +349,7 @@ func (a *azureClient) buildTreeUrl(config *azureOptions, rootObjectHash string)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
|
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := u.Query()
|
q := u.Query()
|
||||||
// projectId={projectId}&recursive=true&fileName={fileName}&$format={$format}&api-version=6.0
|
// projectId={projectId}&recursive=true&fileName={fileName}&$format={$format}&api-version=6.0
|
||||||
q.Set("recursive", "true")
|
q.Set("recursive", "true")
|
||||||
|
@ -361,9 +368,11 @@ func formatReferenceName(name string) string {
|
||||||
if strings.HasPrefix(name, branchPrefix) {
|
if strings.HasPrefix(name, branchPrefix) {
|
||||||
return strings.TrimPrefix(name, branchPrefix)
|
return strings.TrimPrefix(name, branchPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(name, tagPrefix) {
|
if strings.HasPrefix(name, tagPrefix) {
|
||||||
return strings.TrimPrefix(name, tagPrefix)
|
return strings.TrimPrefix(name, tagPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,9 +380,11 @@ func getVersionType(name string) string {
|
||||||
if strings.HasPrefix(name, branchPrefix) {
|
if strings.HasPrefix(name, branchPrefix) {
|
||||||
return "branch"
|
return "branch"
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(name, tagPrefix) {
|
if strings.HasPrefix(name, tagPrefix) {
|
||||||
return "tag"
|
return "tag"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "commit"
|
return "commit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,5 +501,6 @@ func checkAzureStatusCode(err error, code int) error {
|
||||||
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
|
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
|
||||||
return gittypes.ErrAuthenticationFailure
|
return gittypes.ErrAuthenticationFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||||
privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||||
ensureIntegrationTest(t)
|
ensureIntegrationTest(t)
|
||||||
|
@ -107,7 +106,7 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||||
|
|
||||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||||
|
|
||||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||||
|
@ -269,7 +268,7 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||||
|
|
||||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||||
|
|
||||||
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||||
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||||
|
|
|
@ -60,7 +60,7 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||||
|
|
||||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||||
|
|
||||||
repositoryUrl := privateGitRepoURL
|
repositoryUrl := privateGitRepoURL
|
||||||
go service.ListRefs(repositoryUrl, username, accessToken, false)
|
go service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||||
|
@ -224,7 +224,7 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||||
repositoryUrl := privateGitRepoURL
|
repositoryUrl := privateGitRepoURL
|
||||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||||
|
|
||||||
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||||
|
|
|
@ -95,10 +95,12 @@ func getCommitHistoryLength(t *testing.T, err error, dir string) int {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
|
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iter, err := repo.Log(&git.LogOptions{All: true})
|
iter, err := repo.Log(&git.LogOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't get a commit history iterator with error %v", err)
|
t.Fatalf("can't get a commit history iterator with error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
err = iter.ForEach(func(_ *object.Commit) error {
|
err = iter.ForEach(func(_ *object.Commit) error {
|
||||||
count++
|
count++
|
||||||
|
@ -107,6 +109,7 @@ func getCommitHistoryLength(t *testing.T, err error, dir string) int {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't iterate over the commit history with error %v", err)
|
t.Fatalf("can't iterate over the commit history with error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
REPOSITORY_CACHE_SIZE = 4
|
repositoryCacheSize = 4
|
||||||
REPOSITORY_CACHE_TTL = 5 * time.Minute
|
repositoryCacheTTL = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// baseOption provides a minimum group of information to operate a git repository, like git-remote
|
// baseOption provides a minimum group of information to operate a git repository, like git-remote
|
||||||
|
@ -58,7 +58,7 @@ type Service struct {
|
||||||
|
|
||||||
// NewService initializes a new service.
|
// NewService initializes a new service.
|
||||||
func NewService(ctx context.Context) *Service {
|
func NewService(ctx context.Context) *Service {
|
||||||
return newService(ctx, REPOSITORY_CACHE_SIZE, REPOSITORY_CACHE_TTL)
|
return newService(ctx, repositoryCacheSize, repositoryCacheTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(ctx context.Context, cacheSize int, cacheTTL time.Duration) *Service {
|
func newService(ctx context.Context, cacheSize int, cacheTTL time.Duration) *Service {
|
||||||
|
|
|
@ -34,7 +34,7 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
|
||||||
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
|
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
|
hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
|
||||||
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
||||||
if !hashChanged && !forceUpdate {
|
if !hashChanged && !forceUpdate {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
|
|
|
@ -342,12 +342,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27 h1:PceCpp86SDYb3lZHT4KpuBCkmcJMW5x1qrdFNEfAdUo=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a h1:4VGM1OH15fqm5rgki0eLF6vND/NxHfoPt3CA6/YdA0k=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f h1:z/lmLhZMMSIwg70Ap1rPluXNe1vQXH9gfK9K/ols4JA=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7 h1:/i985KPNw0KvVtLhTEPUa86aJMtun5ZPOyFCJzdY+dY=
|
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7 h1:/i985KPNw0KvVtLhTEPUa86aJMtun5ZPOyFCJzdY+dY=
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||||
|
|
|
@ -113,7 +113,9 @@ func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(http.StatusText(resp.StatusCode))
|
return errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
@ -132,7 +134,9 @@ func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(http.StatusText(resp.StatusCode))
|
return errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
@ -151,7 +155,9 @@ func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
@ -182,7 +188,9 @@ func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(http.StatusText(resp.StatusCode))
|
return errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
@ -201,7 +209,9 @@ func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -225,7 +235,9 @@ func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(http.StatusText(resp.StatusCode))
|
return errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
|
|
@ -33,10 +33,13 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
responseBody, readErr := io.ReadAll(response.Body)
|
responseBody, readErr := io.ReadAll(response.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return "", readErr
|
return "", readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
errorResponse := parseError(responseBody)
|
errorResponse := parseError(responseBody)
|
||||||
if errorResponse != nil {
|
if errorResponse != nil {
|
||||||
return "", errorResponse
|
return "", errorResponse
|
||||||
|
|
|
@ -128,6 +128,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
return "", fmt.Errorf("unexpected status code %s", response.Status)
|
return "", fmt.Errorf("unexpected status code %s", response.Status)
|
||||||
|
@ -137,6 +138,8 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
block, _ := pem.Decode(certificate)
|
block, _ := pem.Decode(certificate)
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,6 +103,8 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
responseBody, readErr := io.ReadAll(response.Body)
|
responseBody, readErr := io.ReadAll(response.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
|
@ -132,6 +134,8 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
responseBody, readErr := io.ReadAll(response.Body)
|
responseBody, readErr := io.ReadAll(response.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
|
@ -141,10 +145,12 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
||||||
if response.StatusCode == http.StatusNotFound {
|
if response.StatusCode == http.StatusNotFound {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
errorResponse := parseError(responseBody)
|
errorResponse := parseError(responseBody)
|
||||||
if errorResponse != nil {
|
if errorResponse != nil {
|
||||||
return nil, errorResponse
|
return nil, errorResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unexpected status code %s", response.Status)
|
return nil, fmt.Errorf("unexpected status code %s", response.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||||
|
|
||||||
response := w.Result()
|
response := w.Result()
|
||||||
body, _ := io.ReadAll(response.Body)
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
tmpdir := t.TempDir()
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
@ -89,6 +90,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||||
|
|
||||||
response := w.Result()
|
response := w.Result()
|
||||||
body, _ := io.ReadAll(response.Body)
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
tmpdir := t.TempDir()
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,8 @@ func backup(t *testing.T, h *Handler, password string) []byte {
|
||||||
|
|
||||||
response := w.Result()
|
response := w.Result()
|
||||||
archive, _ := io.ReadAll(response.Body)
|
archive, _ := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
return archive
|
return archive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
@ -26,12 +25,15 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return errors.New("invalid Edge group name")
|
return errors.New("invalid Edge group name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||||
return errors.New("environment is mandatory for a static Edge group")
|
return errors.New("environment is mandatory for a static Edge group")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +58,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
var edgeGroup *portainer.EdgeGroup
|
var edgeGroup *portainer.EdgeGroup
|
||||||
|
|
||||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -101,13 +102,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
|
||||||
return httpErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return httperror.InternalServerError("Unexpected error", err)
|
return txResponse(w, edgeGroup, err)
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, edgeGroup)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/edge"
|
"github.com/portainer/portainer/api/internal/edge"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"github.com/portainer/portainer/api/internal/slices"
|
"github.com/portainer/portainer/api/internal/slices"
|
||||||
|
@ -27,12 +27,15 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return errors.New("invalid Edge group name")
|
return errors.New("invalid Edge group name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||||
return errors.New("environments is mandatory for a static Edge group")
|
return errors.New("environments is mandatory for a static Edge group")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,128 +65,135 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
var edgeGroup *portainer.EdgeGroup
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
|
edgeGroup, err = tx.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||||
} else if err != nil {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
|
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||||
}
|
} else if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||||
if payload.Name != "" {
|
|
||||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
|
|
||||||
}
|
|
||||||
for _, edgeGroup := range edgeGroups {
|
|
||||||
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
|
|
||||||
return httperror.BadRequest("Edge group name must be unique", errors.New("edge group name must be unique"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroup.Name = payload.Name
|
if payload.Name != "" {
|
||||||
}
|
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve environments from database", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to retrieve environment groups from database", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
|
||||||
|
|
||||||
edgeGroup.Dynamic = payload.Dynamic
|
|
||||||
if edgeGroup.Dynamic {
|
|
||||||
edgeGroup.TagIDs = payload.TagIDs
|
|
||||||
} else {
|
|
||||||
endpointIDs := []portainer.EndpointID{}
|
|
||||||
for _, endpointID := range payload.Endpoints {
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpointutils.IsEdgeEndpoint(endpoint) {
|
for _, edgeGroup := range edgeGroups {
|
||||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
|
||||||
|
return httperror.BadRequest("Edge group name must be unique", errors.New("edge group name must be unique"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
edgeGroup.Name = payload.Name
|
||||||
}
|
}
|
||||||
edgeGroup.Endpoints = endpointIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.PartialMatch != nil {
|
endpoints, err := tx.Endpoint().Endpoints()
|
||||||
edgeGroup.PartialMatch = *payload.PartialMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
|
||||||
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
|
|
||||||
|
|
||||||
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs()
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to fetch Edge jobs", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpointID := range endpointsToUpdate {
|
|
||||||
err = handler.updateEndpointStacks(endpointID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
|
return httperror.InternalServerError("Unable to retrieve environments from database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
endpointGroups, err := tx.EndpointGroup().EndpointGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to get Environment from database", err)
|
return httperror.InternalServerError("Unable to retrieve environment groups from database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var operation string
|
edgeGroup.Dynamic = payload.Dynamic
|
||||||
if slices.Contains(newRelatedEndpoints, endpointID) {
|
if edgeGroup.Dynamic {
|
||||||
operation = "add"
|
edgeGroup.TagIDs = payload.TagIDs
|
||||||
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
|
||||||
operation = "remove"
|
|
||||||
} else {
|
} else {
|
||||||
continue
|
endpointIDs := []portainer.EndpointID{}
|
||||||
|
for _, endpointID := range payload.Endpoints {
|
||||||
|
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpointutils.IsEdgeEndpoint(endpoint) {
|
||||||
|
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edgeGroup.Endpoints = endpointIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
if payload.PartialMatch != nil {
|
||||||
|
edgeGroup.PartialMatch = *payload.PartialMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, edgeGroup)
|
newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||||
|
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
|
||||||
|
|
||||||
|
edgeJobs, err := tx.EdgeJob().EdgeJobs()
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to fetch Edge jobs", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointID := range endpointsToUpdate {
|
||||||
|
err = handler.updateEndpointStacks(tx, endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to get Environment from database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var operation string
|
||||||
|
if slices.Contains(newRelatedEndpoints, endpointID) {
|
||||||
|
operation = "add"
|
||||||
|
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
||||||
|
operation = "remove"
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return txResponse(w, edgeGroup, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) error {
|
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) error {
|
||||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -197,7 +207,7 @@ func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) er
|
||||||
|
|
||||||
relation.EdgeStacks = edgeStackSet
|
relation.EdgeStacks = edgeStackSet
|
||||||
|
|
||||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpoint *portainer.Endpoint, edgeJobs []portainer.EdgeJob, operation string) error {
|
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpoint *portainer.Endpoint, edgeJobs []portainer.EdgeJob, operation string) error {
|
||||||
|
|
|
@ -3,11 +3,13 @@ package edgegroups
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||||
|
@ -34,3 +36,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
|
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func txResponse(w http.ResponseWriter, r any, err error) *httperror.HandlerError {
|
||||||
|
if err != nil {
|
||||||
|
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||||
|
return httpErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return httperror.InternalServerError("Unexpected error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, r)
|
||||||
|
}
|
||||||
|
|
|
@ -81,16 +81,14 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
|
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
|
||||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", nil)
|
// EE-5910
|
||||||
|
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
// EE-5910
|
||||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||||
}
|
|
||||||
|
|
||||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||||
|
|
|
@ -30,7 +30,7 @@ var endpointTestCases = []endpointTestCase{
|
||||||
{
|
{
|
||||||
portainer.Endpoint{},
|
portainer.Endpoint{},
|
||||||
portainer.EndpointRelation{},
|
portainer.EndpointRelation{},
|
||||||
http.StatusNotFound,
|
http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
portainer.Endpoint{
|
portainer.Endpoint{
|
||||||
|
@ -43,7 +43,7 @@ var endpointTestCases = []endpointTestCase{
|
||||||
portainer.EndpointRelation{
|
portainer.EndpointRelation{
|
||||||
EndpointID: -1,
|
EndpointID: -1,
|
||||||
},
|
},
|
||||||
http.StatusNotFound,
|
http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
portainer.Endpoint{
|
portainer.Endpoint{
|
||||||
|
|
|
@ -38,7 +38,6 @@ type endpointCreatePayload struct {
|
||||||
AzureAuthenticationKey string
|
AzureAuthenticationKey string
|
||||||
TagIDs []portainer.TagID
|
TagIDs []portainer.TagID
|
||||||
EdgeCheckinInterval int
|
EdgeCheckinInterval int
|
||||||
IsEdgeDevice bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type endpointCreationEnum int
|
type endpointCreationEnum int
|
||||||
|
@ -381,7 +380,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||||
EdgeKey: edgeKey,
|
EdgeKey: edgeKey,
|
||||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
Kubernetes: portainer.KubernetesDefault(),
|
||||||
IsEdgeDevice: payload.IsEdgeDevice,
|
|
||||||
UserTrusted: true,
|
UserTrusted: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,7 +433,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
Snapshots: []portainer.DockerSnapshot{},
|
Snapshots: []portainer.DockerSnapshot{},
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
Kubernetes: portainer.KubernetesDefault(),
|
||||||
IsEdgeDevice: payload.IsEdgeDevice,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := handler.snapshotAndPersistEndpoint(endpoint)
|
err := handler.snapshotAndPersistEndpoint(endpoint)
|
||||||
|
@ -501,7 +498,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
Snapshots: []portainer.DockerSnapshot{},
|
Snapshots: []portainer.DockerSnapshot{},
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
Kubernetes: portainer.KubernetesDefault(),
|
||||||
IsEdgeDevice: payload.IsEdgeDevice,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint.Agent.Version = agentVersion
|
endpoint.Agent.Version = agentVersion
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -128,7 +129,6 @@ func getDockerHubToken(httpClient *client.HTTPClient, registry *portainer.Regist
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) {
|
func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) {
|
||||||
|
|
||||||
requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
|
requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
||||||
|
@ -142,7 +142,9 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errors.New("failed fetching dockerhub limits")
|
return nil, errors.New("failed fetching dockerhub limits")
|
||||||
|
|
|
@ -28,6 +28,10 @@ type endpointSettingsUpdatePayload struct {
|
||||||
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
|
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
|
||||||
// Whether host management features are enabled
|
// Whether host management features are enabled
|
||||||
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"`
|
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"`
|
||||||
|
|
||||||
|
EnableGPUManagement *bool `json:"enableGPUManagement" example:"false"`
|
||||||
|
|
||||||
|
Gpus []portainer.Pair `json:"gpus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -107,6 +111,14 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
|
||||||
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.EnableGPUManagement != nil {
|
||||||
|
endpoint.EnableGPUManagement = *payload.EnableGPUManagement
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Gpus != nil {
|
||||||
|
endpoint.Gpus = payload.Gpus
|
||||||
|
}
|
||||||
|
|
||||||
endpoint.SecuritySettings = securitySettings
|
endpoint.SecuritySettings = securitySettings
|
||||||
|
|
||||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||||
|
|
|
@ -82,7 +82,7 @@ type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @title PortainerCE API
|
// @title PortainerCE API
|
||||||
// @version 2.18.0
|
// @version 2.19.0
|
||||||
// @description.markdown api-description.md
|
// @description.markdown api-description.md
|
||||||
// @termsOfService
|
// @termsOfService
|
||||||
|
|
||||||
|
|
|
@ -48,15 +48,16 @@ func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *h
|
||||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch method {
|
if method == "editor" {
|
||||||
case "editor":
|
|
||||||
return handler.createFDOProfileFromFileContent(w, r)
|
return handler.createFDOProfileFromFileContent(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return httperror.BadRequest("Invalid method. Value must be one of: editor", errors.New("invalid method"))
|
return httperror.BadRequest("Invalid method. Value must be one of: editor", errors.New("invalid method"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
var payload createProfileFromFileContentPayload
|
var payload createProfileFromFileContentPayload
|
||||||
|
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
|
@ -66,6 +67,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError(err.Error(), err)
|
return httperror.InternalServerError(err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isUnique {
|
if !isUnique {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), Err: errors.New("a profile already exists with this name")}
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), Err: errors.New("a profile already exists with this name")}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +82,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.FilePath = filePath
|
profile.FilePath = filePath
|
||||||
profile.DateCreated = time.Now().Unix()
|
profile.DateCreated = time.Now().Unix()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -10,7 +9,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
|
||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -137,12 +135,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
|
|
||||||
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
|
|
||||||
if err != nil {
|
|
||||||
return httperror.InternalServerError("Unable to move git repository directory", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repositoryUsername := ""
|
repositoryUsername := ""
|
||||||
repositoryPassword := ""
|
repositoryPassword := ""
|
||||||
if payload.RepositoryAuthentication {
|
if payload.RepositoryAuthentication {
|
||||||
|
|
|
@ -10,10 +10,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemInfoResponse struct {
|
type systemInfoResponse struct {
|
||||||
Platform plf.ContainerPlatform `json:"platform"`
|
Platform plf.ContainerPlatform `json:"platform"`
|
||||||
EdgeAgents int `json:"edgeAgents"`
|
EdgeAgents int `json:"edgeAgents"`
|
||||||
EdgeDevices int `json:"edgeDevices"`
|
Agents int `json:"agents"`
|
||||||
Agents int `json:"agents"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id systemInfo
|
// @id systemInfo
|
||||||
|
@ -34,7 +33,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||||
|
|
||||||
agents := 0
|
agents := 0
|
||||||
edgeAgents := 0
|
edgeAgents := 0
|
||||||
edgeDevices := 0
|
|
||||||
|
|
||||||
for _, environment := range environments {
|
for _, environment := range environments {
|
||||||
if endpointutils.IsAgentEndpoint(&environment) {
|
if endpointutils.IsAgentEndpoint(&environment) {
|
||||||
|
@ -45,9 +43,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||||
edgeAgents++
|
edgeAgents++
|
||||||
}
|
}
|
||||||
|
|
||||||
if environment.IsEdgeDevice {
|
|
||||||
edgeDevices++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
platform, err := plf.DetermineContainerPlatform()
|
platform, err := plf.DetermineContainerPlatform()
|
||||||
|
@ -56,9 +51,8 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, &systemInfoResponse{
|
return response.JSON(w, &systemInfoResponse{
|
||||||
EdgeAgents: edgeAgents,
|
EdgeAgents: edgeAgents,
|
||||||
EdgeDevices: edgeDevices,
|
Agents: agents,
|
||||||
Agents: agents,
|
Platform: platform,
|
||||||
Platform: platform,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/platform"
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemUpgradePayload struct {
|
type systemUpgradePayload struct {
|
||||||
|
@ -28,13 +30,19 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
|
||||||
|
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
|
||||||
|
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
|
||||||
|
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
|
||||||
|
}
|
||||||
|
|
||||||
// @id systemUpgrade
|
// @id systemUpgrade
|
||||||
// @summary Upgrade Portainer to BE
|
// @summary Upgrade Portainer to BE
|
||||||
// @description Upgrade Portainer to BE
|
// @description Upgrade Portainer to BE
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: administrator
|
||||||
// @tags system
|
// @tags system
|
||||||
// @produce json
|
// @produce json
|
||||||
// @success 200 {object} status "Success"
|
// @success 204 {object} status "Success"
|
||||||
// @router /system/upgrade [post]
|
// @router /system/upgrade [post]
|
||||||
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
payload, err := request.GetPayload[systemUpgradePayload](r)
|
payload, err := request.GetPayload[systemUpgradePayload](r)
|
||||||
|
@ -42,10 +50,40 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.upgradeService.Upgrade(payload.License)
|
environment, err := handler.guessLocalEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Failed to guess local endpoint", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.upgradeService.Upgrade(environment, payload.License)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Failed to upgrade Portainer", err)
|
return httperror.InternalServerError("Failed to upgrade Portainer", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) guessLocalEndpoint() (*portainer.Endpoint, error) {
|
||||||
|
platform, err := platform.DetermineContainerPlatform()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to determine container platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointType, ok := platformToEndpointType[platform]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("failed to determine endpoint type")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.dataStore.Endpoint().Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to retrieve endpoints")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.Type == endpointType {
|
||||||
|
return &endpoint, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("failed to find local endpoint")
|
||||||
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ func (handler *Handler) ifRequestedTemplateExists(payload *filePayload) *httperr
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return httperror.InternalServerError("Invalid template", errors.New("requested template does not exist"))
|
return httperror.InternalServerError("Invalid template", errors.New("requested template does not exist"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +83,7 @@ func (handler *Handler) ifRequestedTemplateExists(payload *filePayload) *httperr
|
||||||
// @router /templates/file [post]
|
// @router /templates/file [post]
|
||||||
func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
var payload filePayload
|
var payload filePayload
|
||||||
|
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
|
@ -112,11 +114,9 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) cleanUp(projectPath string) error {
|
func (handler *Handler) cleanUp(projectPath string) {
|
||||||
err := handler.FileService.RemoveDirectory(projectPath)
|
err := handler.FileService.RemoveDirectory(projectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("HTTP error: unable to cleanup stack creation")
|
log.Debug().Err(err).Msg("HTTP error: unable to cleanup stack creation")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,6 @@ import (
|
||||||
type themePayload struct {
|
type themePayload struct {
|
||||||
// Color represents the color theme of the UI
|
// Color represents the color theme of the UI
|
||||||
Color *string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
Color *string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
|
||||||
SubtleUpgradeButton *bool `json:"subtleUpgradeButton" example:"false"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type userUpdatePayload struct {
|
type userUpdatePayload struct {
|
||||||
|
@ -33,11 +31,11 @@ type userUpdatePayload struct {
|
||||||
|
|
||||||
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.Contains(payload.Username, " ") {
|
if govalidator.Contains(payload.Username, " ") {
|
||||||
return errors.New("Invalid username. Must not contain any whitespace")
|
return errors.New("invalid username. Must not contain any whitespace")
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
||||||
return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
return errors.New("invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -120,10 +118,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||||
if payload.Theme.Color != nil {
|
if payload.Theme.Color != nil {
|
||||||
user.ThemeSettings.Color = *payload.Theme.Color
|
user.ThemeSettings.Color = *payload.Theme.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Theme.SubtleUpgradeButton != nil {
|
|
||||||
user.ThemeSettings.SubtleUpgradeButton = *payload.Theme.SubtleUpgradeButton
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Role != 0 {
|
if payload.Role != 0 {
|
||||||
|
|
|
@ -14,48 +14,57 @@ func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer
|
||||||
_, in, err := websocketConn.ReadMessage()
|
_, in, err := websocketConn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChan <- err
|
errorChan <- err
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = writer.Write(in)
|
_, err = writer.Write(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChan <- err
|
errorChan <- err
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
||||||
|
out := make([]byte, readerBufferSize)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
out := make([]byte, readerBufferSize)
|
n, err := reader.Read(out)
|
||||||
_, err := reader.Read(out)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChan <- err
|
errorChan <- err
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
processedOutput := validString(string(out[:]))
|
processedOutput := validString(string(out[:n]))
|
||||||
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
|
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChan <- err
|
errorChan <- err
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validString(s string) string {
|
func validString(s string) string {
|
||||||
if !utf8.ValidString(s) {
|
if utf8.ValidString(s) {
|
||||||
v := make([]rune, 0, len(s))
|
return s
|
||||||
for i, r := range s {
|
|
||||||
if r == utf8.RuneError {
|
|
||||||
_, size := utf8.DecodeRuneInString(s[i:])
|
|
||||||
if size == 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v = append(v, r)
|
|
||||||
}
|
|
||||||
s = string(v)
|
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
|
v := make([]rune, 0, len(s))
|
||||||
|
|
||||||
|
for i, r := range s {
|
||||||
|
if r == utf8.RuneError {
|
||||||
|
_, size := utf8.DecodeRuneInString(s[i:])
|
||||||
|
if size == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v = append(v, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ func (transport *Transport) applyPortainerContainers(resources []interface{}) ([
|
||||||
responseObject, _ = transport.applyPortainerContainer(responseObject)
|
responseObject, _ = transport.applyPortainerContainer(responseObject)
|
||||||
decoratedResourceData = append(decoratedResourceData, responseObject)
|
decoratedResourceData = append(decoratedResourceData, responseObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decoratedResourceData, nil
|
return decoratedResourceData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,8 +35,10 @@ func (transport *Transport) applyPortainerContainer(resourceObject map[string]in
|
||||||
if !ok {
|
if !ok {
|
||||||
return resourceObject, nil
|
return resourceObject, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resourceId) >= 12 && resourceId[0:12] == portainerContainerId {
|
if len(resourceId) >= 12 && resourceId[0:12] == portainerContainerId {
|
||||||
resourceObject["IsPortainer"] = true
|
resourceObject["IsPortainer"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return resourceObject, nil
|
return resourceObject, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,8 +150,7 @@ func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenMa
|
||||||
func decorateAgentRequest(r *http.Request, dataStore dataservices.DataStore) error {
|
func decorateAgentRequest(r *http.Request, dataStore dataservices.DataStore) error {
|
||||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||||
|
|
||||||
switch {
|
if strings.HasPrefix(requestPath, "/dockerhub") {
|
||||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
|
||||||
return decorateAgentDockerHubRequest(r, dataStore)
|
return decorateAgentDockerHubRequest(r, dataStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,16 @@ func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bo
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||||
|
if endpoint.Kubernetes.Flags.IsServerIngressClassDetected {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
endpoint.Kubernetes.Flags.IsServerIngressClassDetected = true
|
||||||
|
endpointService.UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpoint.ID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
}()
|
||||||
cli, err := factory.GetKubeClient(endpoint)
|
cli, err := factory.GetKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
||||||
|
@ -107,6 +117,16 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||||
|
if endpoint.Kubernetes.Flags.IsServerMetricsDetected {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||||
|
endpointService.UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpoint.ID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
}()
|
||||||
cli, err := factory.GetKubeClient(endpoint)
|
cli, err := factory.GetKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
||||||
|
@ -118,11 +138,6 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
endpoint.Kubernetes.Configuration.UseServerMetrics = true
|
endpoint.Kubernetes.Configuration.UseServerMetrics = true
|
||||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
|
||||||
err = endpointService.UpdateEndpoint(
|
|
||||||
portainer.EndpointID(endpoint.ID),
|
|
||||||
endpoint,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
|
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
|
||||||
return
|
return
|
||||||
|
@ -158,6 +173,16 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||||
|
if endpoint.Kubernetes.Flags.IsServerStorageDetected {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||||
|
endpointService.UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpoint.ID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
}()
|
||||||
log.Info().Msg("attempting to detect storage classes in the cluster")
|
log.Info().Msg("attempting to detect storage classes in the cluster")
|
||||||
err := storageDetect(endpoint, endpointService, factory)
|
err := storageDetect(endpoint, endpointService, factory)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
package upgrade
|
package upgrade
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cbroglie/mustache"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
libstack "github.com/portainer/docker-compose-wrapper"
|
libstack "github.com/portainer/docker-compose-wrapper"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
|
||||||
"github.com/portainer/portainer/api/platform"
|
"github.com/portainer/portainer/api/platform"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -36,19 +26,23 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Upgrade(licenseKey string) error
|
Upgrade(environment *portainer.Endpoint, licenseKey string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
composeDeployer libstack.Deployer
|
composeDeployer libstack.Deployer
|
||||||
isUpdating bool
|
kubernetesClientFactory *cli.ClientFactory
|
||||||
platform platform.ContainerPlatform
|
|
||||||
assetsPath string
|
isUpdating bool
|
||||||
|
platform platform.ContainerPlatform
|
||||||
|
|
||||||
|
assetsPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
assetsPath string,
|
assetsPath string,
|
||||||
composeDeployer libstack.Deployer,
|
composeDeployer libstack.Deployer,
|
||||||
|
kubernetesClientFactory *cli.ClientFactory,
|
||||||
) (Service, error) {
|
) (Service, error) {
|
||||||
platform, err := platform.DetermineContainerPlatform()
|
platform, err := platform.DetermineContainerPlatform()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -56,13 +50,14 @@ func NewService(
|
||||||
}
|
}
|
||||||
|
|
||||||
return &service{
|
return &service{
|
||||||
assetsPath: assetsPath,
|
assetsPath: assetsPath,
|
||||||
composeDeployer: composeDeployer,
|
composeDeployer: composeDeployer,
|
||||||
platform: platform,
|
kubernetesClientFactory: kubernetesClientFactory,
|
||||||
|
platform: platform,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *service) Upgrade(licenseKey string) error {
|
func (service *service) Upgrade(environment *portainer.Endpoint, licenseKey string) error {
|
||||||
service.isUpdating = true
|
service.isUpdating = true
|
||||||
|
|
||||||
switch service.platform {
|
switch service.platform {
|
||||||
|
@ -70,113 +65,9 @@ func (service *service) Upgrade(licenseKey string) error {
|
||||||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone")
|
return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone")
|
||||||
case platform.PlatformDockerSwarm:
|
case platform.PlatformDockerSwarm:
|
||||||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm")
|
return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm")
|
||||||
// case platform.PlatformKubernetes:
|
case platform.PlatformKubernetes:
|
||||||
// case platform.PlatformPodman:
|
return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion)
|
||||||
// case platform.PlatformNomad:
|
|
||||||
// default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("unsupported platform %s", service.platform)
|
return fmt.Errorf("unsupported platform %s", service.platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *service) upgradeDocker(licenseKey, version, envType string) error {
|
|
||||||
ctx := context.TODO()
|
|
||||||
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
|
|
||||||
|
|
||||||
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
|
||||||
if portainerImagePrefix == "" {
|
|
||||||
portainerImagePrefix = "portainer/portainer-ee"
|
|
||||||
}
|
|
||||||
|
|
||||||
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
|
||||||
|
|
||||||
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
|
||||||
|
|
||||||
if err := service.checkImage(ctx, image, skipPullImage != ""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
|
||||||
"image": image,
|
|
||||||
"skip_pull_image": skipPullImage,
|
|
||||||
"updater_image": os.Getenv(updaterImageEnvVar),
|
|
||||||
"license": licenseKey,
|
|
||||||
"envType": envType,
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Debug().
|
|
||||||
Str("composeFile", composeFile).
|
|
||||||
Msg("Compose file for upgrade")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to render upgrade template")
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
|
||||||
timeId := time.Now().Unix()
|
|
||||||
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId))
|
|
||||||
|
|
||||||
r := bytes.NewReader([]byte(composeFile))
|
|
||||||
|
|
||||||
err = filesystem.CreateFile(filePath, r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to create upgrade compose file")
|
|
||||||
}
|
|
||||||
|
|
||||||
projectName := fmt.Sprintf(
|
|
||||||
"portainer-upgrade-%d-%s",
|
|
||||||
timeId,
|
|
||||||
strings.Replace(version, ".", "-", -1))
|
|
||||||
|
|
||||||
err = service.composeDeployer.Deploy(
|
|
||||||
ctx,
|
|
||||||
[]string{filePath},
|
|
||||||
libstack.DeployOptions{
|
|
||||||
ForceRecreate: true,
|
|
||||||
AbortOnContainerExit: true,
|
|
||||||
Options: libstack.Options{
|
|
||||||
ProjectName: projectName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// optimally, server was restarted by the updater, so we should not reach this point
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to deploy upgrade stack")
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("upgrade failed: server should have been restarted by the updater")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *service) checkImage(ctx context.Context, image string, skipPullImage bool) error {
|
|
||||||
cli, err := docker.CreateClientFromEnv()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to create docker client")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skipPullImage {
|
|
||||||
filters := filters.NewArgs()
|
|
||||||
filters.Add("reference", image)
|
|
||||||
images, err := cli.ImageList(ctx, types.ImageListOptions{
|
|
||||||
Filters: filters,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to list images")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(images) == 0 {
|
|
||||||
return errors.Errorf("image %s not found locally", image)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
// check if available on registry
|
|
||||||
_, err := cli.DistributionInspect(ctx, image, "")
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("image %s not found on registry", image)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
libstack "github.com/portainer/docker-compose-wrapper"
|
||||||
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
|
||||||
|
"github.com/cbroglie/mustache"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (service *service) upgradeDocker(licenseKey, version, envType string) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
|
||||||
|
|
||||||
|
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||||
|
if portainerImagePrefix == "" {
|
||||||
|
portainerImagePrefix = "portainer/portainer-ee"
|
||||||
|
}
|
||||||
|
|
||||||
|
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||||
|
|
||||||
|
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
||||||
|
|
||||||
|
if err := service.checkImageForDocker(ctx, image, skipPullImage != ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
||||||
|
"image": image,
|
||||||
|
"skip_pull_image": skipPullImage,
|
||||||
|
"updater_image": os.Getenv(updaterImageEnvVar),
|
||||||
|
"license": licenseKey,
|
||||||
|
"envType": envType,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("composeFile", composeFile).
|
||||||
|
Msg("Compose file for upgrade")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to render upgrade template")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
timeId := time.Now().Unix()
|
||||||
|
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId))
|
||||||
|
|
||||||
|
r := bytes.NewReader([]byte(composeFile))
|
||||||
|
|
||||||
|
err = filesystem.CreateFile(filePath, r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create upgrade compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectName := fmt.Sprintf(
|
||||||
|
"portainer-upgrade-%d-%s",
|
||||||
|
timeId,
|
||||||
|
strings.ReplaceAll(version, ".", "-"))
|
||||||
|
|
||||||
|
err = service.composeDeployer.Deploy(
|
||||||
|
ctx,
|
||||||
|
[]string{filePath},
|
||||||
|
libstack.DeployOptions{
|
||||||
|
ForceRecreate: true,
|
||||||
|
AbortOnContainerExit: true,
|
||||||
|
Options: libstack.Options{
|
||||||
|
ProjectName: projectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// optimally, server was restarted by the updater, so we should not reach this point
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to deploy upgrade stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("upgrade failed: server should have been restarted by the updater")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *service) checkImageForDocker(ctx context.Context, image string, skipPullImage bool) error {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create docker client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipPullImage {
|
||||||
|
filters := filters.NewArgs()
|
||||||
|
filters.Add("reference", image)
|
||||||
|
images, err := cli.ImageList(ctx, types.ImageListOptions{
|
||||||
|
Filters: filters,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to list images")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(images) == 0 {
|
||||||
|
return errors.Errorf("image %s not found locally", image)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// check if available on registry
|
||||||
|
_, err := cli.DistributionInspect(ctx, image, "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("image %s not found on registry", image)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ptr[T any](i T) *T { return &i }
|
||||||
|
|
||||||
|
func (service *service) upgradeKubernetes(environment *portainer.Endpoint, licenseKey, version string) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
kubeCLI, err := service.kubernetesClientFactory.CreateClient(environment)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed to get kubernetes client")
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := "portainer"
|
||||||
|
taskName := fmt.Sprintf("portainer-upgrade-%d", time.Now().Unix())
|
||||||
|
|
||||||
|
jobsCli := kubeCLI.BatchV1().Jobs(namespace)
|
||||||
|
|
||||||
|
updaterImage := os.Getenv(updaterImageEnvVar)
|
||||||
|
if updaterImage == "" {
|
||||||
|
updaterImage = "portainer/portainer-updater:latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||||
|
if portainerImagePrefix == "" {
|
||||||
|
portainerImagePrefix = "portainer/portainer-ee"
|
||||||
|
}
|
||||||
|
|
||||||
|
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||||
|
|
||||||
|
if err := service.checkImageForKubernetes(ctx, kubeCLI, namespace, image); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := jobsCli.Create(ctx, &batchv1.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: taskName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
TTLSecondsAfterFinished: ptr[int32](5 * 60), // cleanup after 5 minutes
|
||||||
|
BackoffLimit: ptr[int32](0),
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
|
||||||
|
RestartPolicy: "Never",
|
||||||
|
ServiceAccountName: "portainer-sa-clusteradmin",
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: taskName,
|
||||||
|
Image: updaterImage,
|
||||||
|
Args: []string{
|
||||||
|
"--pretty-log",
|
||||||
|
"--log-level", "DEBUG",
|
||||||
|
"portainer",
|
||||||
|
"--env-type", "kubernetes",
|
||||||
|
"--image", image,
|
||||||
|
"--license", licenseKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed to create upgrade job")
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := jobsCli.Watch(ctx, metav1.ListOptions{
|
||||||
|
FieldSelector: "metadata.name=" + taskName,
|
||||||
|
TimeoutSeconds: ptr[int64](60),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed to watch upgrade job")
|
||||||
|
}
|
||||||
|
|
||||||
|
for event := range watcher.ResultChan() {
|
||||||
|
job, ok := event.Object.(*batchv1.Job)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range job.Status.Conditions {
|
||||||
|
if c.Type == batchv1.JobComplete {
|
||||||
|
log.Debug().
|
||||||
|
Str("job", job.Name).
|
||||||
|
Msg("Upgrade job completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Type == batchv1.JobFailed {
|
||||||
|
return fmt.Errorf("upgrade failed: %s", c.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("job", job.Name).
|
||||||
|
Msg("Upgrade job created")
|
||||||
|
|
||||||
|
return errors.New("upgrade failed: server should have been restarted by the updater")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *service) checkImageForKubernetes(ctx context.Context, kubeCLI *kubernetes.Clientset, namespace, image string) error {
|
||||||
|
podsCli := kubeCLI.CoreV1().Pods(namespace)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("image", image).
|
||||||
|
Msg("Checking image")
|
||||||
|
|
||||||
|
podName := fmt.Sprintf("portainer-image-check-%d", time.Now().Unix())
|
||||||
|
_, err := podsCli.Create(ctx, &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: podName,
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
RestartPolicy: "Never",
|
||||||
|
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: fmt.Sprint(podName, "-container"),
|
||||||
|
Image: image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("failed to create image check pod")
|
||||||
|
return errors.WithMessage(err, "failed to create image check pod")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Debug().
|
||||||
|
Str("pod", podName).
|
||||||
|
Msg("Deleting image check pod")
|
||||||
|
|
||||||
|
if err := podsCli.Delete(ctx, podName, metav1.DeleteOptions{}); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("failed to delete image check pod")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("image", image).
|
||||||
|
Int("try", i).
|
||||||
|
Msg("Checking image")
|
||||||
|
|
||||||
|
i++
|
||||||
|
|
||||||
|
pod, err := podsCli.Get(ctx, podName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, "failed to get image check pod")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, containerStatus := range pod.Status.ContainerStatuses {
|
||||||
|
if containerStatus.Ready {
|
||||||
|
log.Debug().
|
||||||
|
Str("image", image).
|
||||||
|
Str("pod", podName).
|
||||||
|
Msg("Image check container ready, assuming image is available")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerStatus.State.Waiting != nil {
|
||||||
|
if containerStatus.State.Waiting.Reason == "ErrImagePull" || containerStatus.State.Waiting.Reason == "ImagePullBackOff" {
|
||||||
|
log.Debug().
|
||||||
|
Str("image", image).
|
||||||
|
Str("pod", podName).
|
||||||
|
Str("reason", containerStatus.State.Waiting.Reason).
|
||||||
|
Str("message", containerStatus.State.Waiting.Message).
|
||||||
|
Str("container", containerStatus.Name).
|
||||||
|
Msg("Image check container failed because of missing image")
|
||||||
|
return fmt.Errorf("image %s not found", image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,8 @@ import (
|
||||||
|
|
||||||
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||||
kcl.lock.Lock()
|
kcl.mu.Lock()
|
||||||
defer kcl.lock.Unlock()
|
defer kcl.mu.Unlock()
|
||||||
|
|
||||||
policies, err := kcl.GetNamespaceAccessPolicies()
|
policies, err := kcl.GetNamespaceAccessPolicies()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -42,6 +42,7 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return policies, nil
|
return policies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -40,7 +39,6 @@ func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConf
|
||||||
k := &KubeClient{
|
k := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(),
|
cli: kfake.NewSimpleClientset(),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &ktypes.ConfigMap{
|
config := &ktypes.ConfigMap{
|
||||||
|
|
|
@ -7,17 +7,20 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cmap "github.com/orcaman/concurrent-map"
|
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultKubeClientQPS = 30
|
||||||
|
DefaultKubeClientBurst = 100
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ClientFactory is used to create Kubernetes clients
|
// ClientFactory is used to create Kubernetes clients
|
||||||
ClientFactory struct {
|
ClientFactory struct {
|
||||||
|
@ -25,16 +28,17 @@ type (
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
instanceID string
|
instanceID string
|
||||||
endpointClients cmap.ConcurrentMap
|
endpointClients map[string]*KubeClient
|
||||||
endpointProxyClients *cache.Cache
|
endpointProxyClients *cache.Cache
|
||||||
AddrHTTPS string
|
AddrHTTPS string
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubeClient represent a service used to execute Kubernetes operations
|
// KubeClient represent a service used to execute Kubernetes operations
|
||||||
KubeClient struct {
|
KubeClient struct {
|
||||||
cli kubernetes.Interface
|
cli kubernetes.Interface
|
||||||
instanceID string
|
instanceID string
|
||||||
lock *sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,7 +57,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
reverseTunnelService: reverseTunnelService,
|
reverseTunnelService: reverseTunnelService,
|
||||||
instanceID: instanceID,
|
instanceID: instanceID,
|
||||||
endpointClients: cmap.New(),
|
endpointClients: make(map[string]*KubeClient),
|
||||||
endpointProxyClients: cache.New(timeout, timeout),
|
endpointProxyClients: cache.New(timeout, timeout),
|
||||||
AddrHTTPS: addrHTTPS,
|
AddrHTTPS: addrHTTPS,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -65,82 +69,87 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
|
||||||
|
|
||||||
// Remove the cached kube client so a new one can be created
|
// Remove the cached kube client so a new one can be created
|
||||||
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
||||||
factory.endpointClients.Remove(strconv.Itoa(int(endpointID)))
|
factory.mu.Lock()
|
||||||
|
delete(factory.endpointClients, strconv.Itoa(int(endpointID)))
|
||||||
|
factory.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
||||||
// If no client is registered, it will create a new client, register it, and returns it.
|
// If no client is registered, it will create a new client, register it, and returns it.
|
||||||
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||||
|
factory.mu.Lock()
|
||||||
|
defer factory.mu.Unlock()
|
||||||
|
|
||||||
key := strconv.Itoa(int(endpoint.ID))
|
key := strconv.Itoa(int(endpoint.ID))
|
||||||
client, ok := factory.endpointClients.Get(key)
|
client, ok := factory.endpointClients[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
client, err := factory.createCachedAdminKubeClient(endpoint)
|
var err error
|
||||||
|
|
||||||
|
client, err = factory.createCachedAdminKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
factory.endpointClients.Set(key, client)
|
factory.endpointClients[key] = client
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.(portainer.KubeClient), nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
||||||
// calling SetProxyKubeClient before first. It is normally, called the
|
// calling SetProxyKubeClient before first. It is normally, called the
|
||||||
// kubernetes middleware.
|
// kubernetes middleware.
|
||||||
func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (portainer.KubeClient, bool) {
|
func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (*KubeClient, bool) {
|
||||||
client, ok := factory.endpointProxyClients.Get(endpointID + "." + token)
|
client, ok := factory.endpointProxyClients.Get(endpointID + "." + token)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return client.(portainer.KubeClient), true
|
|
||||||
|
return client.(*KubeClient), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProxyKubeClient stores a kubeclient in the cache.
|
// SetProxyKubeClient stores a kubeclient in the cache.
|
||||||
func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli portainer.KubeClient) {
|
func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli *KubeClient) {
|
||||||
factory.endpointProxyClients.Set(endpointID+"."+token, cli, 0)
|
factory.endpointProxyClients.Set(endpointID+"."+token, cli, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
||||||
// Kubernetes config.
|
// Kubernetes config.
|
||||||
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (portainer.KubeClient, error) {
|
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (*KubeClient, error) {
|
||||||
config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
|
config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cliConfig, err := config.ClientConfig()
|
cliConfig, err := config.ClientConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cliConfig.QPS = DefaultKubeClientQPS
|
||||||
|
cliConfig.Burst = DefaultKubeClientBurst
|
||||||
|
|
||||||
cli, err := kubernetes.NewForConfig(cliConfig)
|
cli, err := kubernetes.NewForConfig(cliConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubecli := &KubeClient{
|
return &KubeClient{
|
||||||
cli: cli,
|
cli: cli,
|
||||||
instanceID: factory.instanceID,
|
instanceID: factory.instanceID,
|
||||||
lock: &sync.Mutex{},
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
return kubecli, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||||
cli, err := factory.CreateClient(endpoint)
|
cli, err := factory.CreateClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kubecli := &KubeClient{
|
return &KubeClient{
|
||||||
cli: cli,
|
cli: cli,
|
||||||
instanceID: factory.instanceID,
|
instanceID: factory.instanceID,
|
||||||
lock: &sync.Mutex{},
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
return kubecli, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateClient returns a pointer to a new Clientset instance
|
// CreateClient returns a pointer to a new Clientset instance
|
||||||
|
@ -199,7 +208,10 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Insecure = true
|
config.Insecure = true
|
||||||
|
config.QPS = DefaultKubeClientQPS
|
||||||
|
config.Burst = DefaultKubeClientBurst
|
||||||
|
|
||||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||||
return &agentHeaderRoundTripper{
|
return &agentHeaderRoundTripper{
|
||||||
|
@ -218,30 +230,13 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.QPS = DefaultKubeClientQPS
|
||||||
|
config.Burst = DefaultKubeClientBurst
|
||||||
|
|
||||||
return kubernetes.NewForConfig(config)
|
return kubernetes.NewForConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) PostInitMigrateIngresses() error {
|
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error {
|
||||||
endpoints, err := factory.dataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := range endpoints {
|
|
||||||
// Early exit if we do not need to migrate!
|
|
||||||
if endpoints[i].PostInitMigrations.MigrateIngresses == false {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := factory.migrateEndpointIngresses(&endpoints[i])
|
|
||||||
if err != nil {
|
|
||||||
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *ClientFactory) migrateEndpointIngresses(e *portainer.Endpoint) error {
|
|
||||||
// classes is a list of controllers which have been manually added to the
|
// classes is a list of controllers which have been manually added to the
|
||||||
// cluster setup view. These need to all be allowed globally, but then
|
// cluster setup view. These need to all be allowed globally, but then
|
||||||
// blocked in specific namespaces which they were not previously allowed in.
|
// blocked in specific namespaces which they were not previously allowed in.
|
||||||
|
|
|
@ -3,7 +3,6 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -19,7 +18,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, true)
|
err := kcl.ToggleSystemState(nsName, true)
|
||||||
|
@ -37,12 +35,10 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(),
|
cli: kfake.NewSimpleClientset(),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, true)
|
err := kcl.ToggleSystemState(nsName, true)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("if called with the same state, should skip (exit without error)", func(t *testing.T) {
|
t.Run("if called with the same state, should skip (exit without error)", func(t *testing.T) {
|
||||||
|
@ -61,7 +57,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
systemNamespaceLabel: strconv.FormatBool(test.isSystem),
|
systemNamespaceLabel: strconv.FormatBool(test.isSystem),
|
||||||
}}}),
|
}}}),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, test.isSystem)
|
err := kcl.ToggleSystemState(nsName, test.isSystem)
|
||||||
|
@ -81,7 +76,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, true)
|
err := kcl.ToggleSystemState(nsName, true)
|
||||||
|
@ -102,7 +96,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, false)
|
err := kcl.ToggleSystemState(nsName, false)
|
||||||
|
@ -125,7 +118,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
systemNamespaceLabel: "true",
|
systemNamespaceLabel: "true",
|
||||||
}}}),
|
}}}),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, false)
|
err := kcl.ToggleSystemState(nsName, false)
|
||||||
|
@ -159,7 +151,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
kcl := &KubeClient{
|
kcl := &KubeClient{
|
||||||
cli: kfake.NewSimpleClientset(namespace, config),
|
cli: kfake.NewSimpleClientset(namespace, config),
|
||||||
instanceID: "instance",
|
instanceID: "instance",
|
||||||
lock: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := kcl.ToggleSystemState(nsName, true)
|
err := kcl.ToggleSystemState(nsName, true)
|
||||||
|
@ -178,6 +169,5 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||||
actualPolicies, err := kcl.GetNamespaceAccessPolicies()
|
actualPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||||
assert.NoError(t, err, "failed to fetch policies")
|
assert.NoError(t, err, "failed to fetch policies")
|
||||||
assert.Equal(t, expectedPolicies, actualPolicies)
|
assert.Equal(t, expectedPolicies, actualPolicies)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port
|
||||||
|
|
||||||
// When the api call is internal, the baseURL should not be used.
|
// When the api call is internal, the baseURL should not be used.
|
||||||
if hostURL == "localhost" {
|
if hostURL == "localhost" {
|
||||||
hostURL = hostURL + service.httpsBindAddr
|
hostURL += service.httpsBindAddr
|
||||||
baseURL = "/"
|
baseURL = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,12 +121,13 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -41,10 +41,12 @@ func DetermineContainerPlatform() (ContainerPlatform, error) {
|
||||||
if podmanModeEnvVar == "1" {
|
if podmanModeEnvVar == "1" {
|
||||||
return PlatformPodman, nil
|
return PlatformPodman, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
|
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
|
||||||
if serviceHostKubernetesEnvVar != "" {
|
if serviceHostKubernetesEnvVar != "" {
|
||||||
return PlatformKubernetes, nil
|
return PlatformKubernetes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nomadJobName := os.Getenv(NomadJobName)
|
nomadJobName := os.Getenv(NomadJobName)
|
||||||
if nomadJobName != "" {
|
if nomadJobName != "" {
|
||||||
return PlatformNomad, nil
|
return PlatformNomad, nil
|
||||||
|
|
|
@ -225,7 +225,7 @@ type (
|
||||||
// It contains some information of Docker's ContainerJSON struct
|
// It contains some information of Docker's ContainerJSON struct
|
||||||
DockerContainerSnapshot struct {
|
DockerContainerSnapshot struct {
|
||||||
types.Container
|
types.Container
|
||||||
Env []string `json:"Env"`
|
Env []string `json:"Env,omitempty"` // EE-5240
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||||
|
@ -388,8 +388,7 @@ type (
|
||||||
LastCheckInDate int64
|
LastCheckInDate int64
|
||||||
// QueryDate of each query with the endpoints list
|
// QueryDate of each query with the endpoints list
|
||||||
QueryDate int64
|
QueryDate int64
|
||||||
// IsEdgeDevice marks if the environment was created as an EdgeDevice
|
|
||||||
IsEdgeDevice bool
|
|
||||||
// Whether the device has been trusted or not by the user
|
// Whether the device has been trusted or not by the user
|
||||||
UserTrusted bool
|
UserTrusted bool
|
||||||
|
|
||||||
|
@ -402,6 +401,8 @@ type (
|
||||||
Version string `example:"1.0.0"`
|
Version string `example:"1.0.0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnableGPUManagement bool `json:"EnableGPUManagement"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
TLS bool `json:"TLS,omitempty"`
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
@ -415,6 +416,9 @@ type (
|
||||||
|
|
||||||
// Deprecated in DBVersion == 22
|
// Deprecated in DBVersion == 22
|
||||||
Tags []string `json:"Tags"`
|
Tags []string `json:"Tags"`
|
||||||
|
|
||||||
|
// Deprecated v2.18
|
||||||
|
IsEdgeDevice bool
|
||||||
}
|
}
|
||||||
|
|
||||||
EnvironmentEdgeSettings struct {
|
EnvironmentEdgeSettings struct {
|
||||||
|
@ -502,6 +506,7 @@ type (
|
||||||
// EndpointPostInitMigrations
|
// EndpointPostInitMigrations
|
||||||
EndpointPostInitMigrations struct {
|
EndpointPostInitMigrations struct {
|
||||||
MigrateIngresses bool `json:"MigrateIngresses"`
|
MigrateIngresses bool `json:"MigrateIngresses"`
|
||||||
|
MigrateGPUs bool `json:"MigrateGPUs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension represents a deprecated Portainer extension
|
// Extension represents a deprecated Portainer extension
|
||||||
|
@ -585,9 +590,12 @@ type (
|
||||||
Flags KubernetesFlags `json:"Flags"`
|
Flags KubernetesFlags `json:"Flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KubernetesFlags are used to detect if we need to run initial cluster
|
||||||
|
// detection again.
|
||||||
KubernetesFlags struct {
|
KubernetesFlags struct {
|
||||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
IsServerIngressClassDetected bool `json:"IsServerIngressClassDetected"`
|
||||||
|
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
||||||
|
@ -1283,8 +1291,6 @@ type (
|
||||||
UserThemeSettings struct {
|
UserThemeSettings struct {
|
||||||
// Color represents the color theme of the UI
|
// Color represents the color theme of the UI
|
||||||
Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
|
||||||
SubtleUpgradeButton bool `json:"subtleUpgradeButton"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webhook represents a url webhook that can be used to update a service
|
// Webhook represents a url webhook that can be used to update a service
|
||||||
|
@ -1507,7 +1513,7 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "2.18.0"
|
APIVersion = "2.19.0"
|
||||||
// Edition is what this edition of Portainer is called
|
// Edition is what this edition of Portainer is called
|
||||||
Edition = PortainerCE
|
Edition = PortainerCE
|
||||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||||
|
@ -1554,7 +1560,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of supported features
|
// List of supported features
|
||||||
var SupportedFeatureFlags = []featureflags.Feature{}
|
var SupportedFeatureFlags = []featureflags.Feature{
|
||||||
|
"fdo",
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_ AuthenticationMethod = iota
|
_ AuthenticationMethod = iota
|
||||||
|
|
|
@ -24,6 +24,7 @@ func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
|
||||||
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||||
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
|
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePaths
|
return filePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,8 +120,6 @@
|
||||||
--bg-navtabs-hover-color: var(--grey-16);
|
--bg-navtabs-hover-color: var(--grey-16);
|
||||||
--bg-nav-tab-active-color: var(--ui-gray-4);
|
--bg-nav-tab-active-color: var(--ui-gray-4);
|
||||||
--bg-table-selected-color: var(--grey-14);
|
--bg-table-selected-color: var(--grey-14);
|
||||||
--bg-codemirror-color: var(--white-color);
|
|
||||||
--bg-codemirror-gutters-color: var(--grey-17);
|
|
||||||
--bg-dropdown-menu-color: var(--white-color);
|
--bg-dropdown-menu-color: var(--white-color);
|
||||||
--bg-log-viewer-color: var(--white-color);
|
--bg-log-viewer-color: var(--white-color);
|
||||||
--bg-log-line-selected-color: var(--grey-18);
|
--bg-log-line-selected-color: var(--grey-18);
|
||||||
|
@ -136,7 +134,6 @@
|
||||||
--bg-item-highlighted-color: var(--grey-21);
|
--bg-item-highlighted-color: var(--grey-21);
|
||||||
--bg-item-highlighted-null-color: var(--grey-14);
|
--bg-item-highlighted-null-color: var(--grey-14);
|
||||||
--bg-panel-body-color: var(--white-color);
|
--bg-panel-body-color: var(--white-color);
|
||||||
--bg-codemirror-selected-color: var(--grey-22);
|
|
||||||
--bg-tooltip-color: var(--ui-gray-11);
|
--bg-tooltip-color: var(--ui-gray-11);
|
||||||
--bg-input-sm-color: var(--white-color);
|
--bg-input-sm-color: var(--white-color);
|
||||||
--bg-app-datatable-thead: var(--grey-23);
|
--bg-app-datatable-thead: var(--grey-23);
|
||||||
|
@ -182,11 +179,7 @@
|
||||||
--text-navtabs-color: var(--grey-7);
|
--text-navtabs-color: var(--grey-7);
|
||||||
--text-navtabs-hover-color: var(--grey-6);
|
--text-navtabs-hover-color: var(--grey-6);
|
||||||
--text-nav-tab-active-color: var(--grey-25);
|
--text-nav-tab-active-color: var(--grey-25);
|
||||||
--text-cm-default-color: var(--blue-1);
|
|
||||||
--text-cm-meta-color: var(--black-color);
|
|
||||||
--text-cm-string-color: var(--red-3);
|
|
||||||
--text-cm-number-color: var(--green-1);
|
|
||||||
--text-codemirror-color: var(--black-color);
|
|
||||||
--text-dropdown-menu-color: var(--grey-6);
|
--text-dropdown-menu-color: var(--grey-6);
|
||||||
--text-log-viewer-color: var(--black-color);
|
--text-log-viewer-color: var(--black-color);
|
||||||
--text-json-tree-color: var(--blue-3);
|
--text-json-tree-color: var(--blue-3);
|
||||||
|
@ -224,7 +217,6 @@
|
||||||
--border-md-checkbox-color: var(--grey-19);
|
--border-md-checkbox-color: var(--grey-19);
|
||||||
--border-modal-header-color: var(--grey-45);
|
--border-modal-header-color: var(--grey-45);
|
||||||
--border-navtabs-color: var(--ui-white);
|
--border-navtabs-color: var(--ui-white);
|
||||||
--border-codemirror-cursor-color: var(--black-color);
|
|
||||||
--border-pre-color: var(--grey-43);
|
--border-pre-color: var(--grey-43);
|
||||||
--border-pagination-span-color: var(--ui-white);
|
--border-pagination-span-color: var(--ui-white);
|
||||||
--border-pagination-hover-color: var(--ui-white);
|
--border-pagination-hover-color: var(--ui-white);
|
||||||
|
@ -281,9 +273,6 @@
|
||||||
--bg-card-color: var(--grey-1);
|
--bg-card-color: var(--grey-1);
|
||||||
--bg-checkbox-border-color: var(--grey-8);
|
--bg-checkbox-border-color: var(--grey-8);
|
||||||
--bg-code-color: var(--grey-2);
|
--bg-code-color: var(--grey-2);
|
||||||
--bg-codemirror-color: var(--grey-2);
|
|
||||||
--bg-codemirror-gutters-color: var(--grey-3);
|
|
||||||
--bg-codemirror-selected-color: var(--grey-3);
|
|
||||||
--bg-dropdown-menu-color: var(--ui-gray-warm-8);
|
--bg-dropdown-menu-color: var(--ui-gray-warm-8);
|
||||||
--bg-main-color: var(--grey-2);
|
--bg-main-color: var(--grey-2);
|
||||||
--bg-sidebar-color: var(--grey-1);
|
--bg-sidebar-color: var(--grey-1);
|
||||||
|
@ -361,11 +350,7 @@
|
||||||
--text-navtabs-color: var(--grey-8);
|
--text-navtabs-color: var(--grey-8);
|
||||||
--text-navtabs-hover-color: var(--grey-9);
|
--text-navtabs-hover-color: var(--grey-9);
|
||||||
--text-nav-tab-active-color: var(--white-color);
|
--text-nav-tab-active-color: var(--white-color);
|
||||||
--text-cm-default-color: var(--blue-10);
|
|
||||||
--text-cm-meta-color: var(--white-color);
|
|
||||||
--text-cm-string-color: var(--red-5);
|
|
||||||
--text-cm-number-color: var(--green-2);
|
|
||||||
--text-codemirror-color: var(--white-color);
|
|
||||||
--text-dropdown-menu-color: var(--white-color);
|
--text-dropdown-menu-color: var(--white-color);
|
||||||
--text-log-viewer-color: var(--white-color);
|
--text-log-viewer-color: var(--white-color);
|
||||||
--text-json-tree-color: var(--grey-40);
|
--text-json-tree-color: var(--grey-40);
|
||||||
|
@ -403,7 +388,6 @@
|
||||||
--border-md-checkbox-color: var(--grey-41);
|
--border-md-checkbox-color: var(--grey-41);
|
||||||
--border-modal-header-color: var(--grey-1);
|
--border-modal-header-color: var(--grey-1);
|
||||||
--border-navtabs-color: var(--grey-38);
|
--border-navtabs-color: var(--grey-38);
|
||||||
--border-codemirror-cursor-color: var(--white-color);
|
|
||||||
--border-pre-color: var(--grey-3);
|
--border-pre-color: var(--grey-3);
|
||||||
--border-blocklist: var(--ui-gray-9);
|
--border-blocklist: var(--ui-gray-9);
|
||||||
--border-blocklist-item-selected-color: var(--grey-38);
|
--border-blocklist-item-selected-color: var(--grey-38);
|
||||||
|
@ -468,15 +452,12 @@
|
||||||
--bg-switch-box-color: var(--grey-53);
|
--bg-switch-box-color: var(--grey-53);
|
||||||
--bg-panel-body-color: var(--black-color);
|
--bg-panel-body-color: var(--black-color);
|
||||||
--bg-dropdown-menu-color: var(--ui-gray-warm-8);
|
--bg-dropdown-menu-color: var(--ui-gray-warm-8);
|
||||||
--bg-codemirror-selected-color: var(--grey-3);
|
|
||||||
--bg-motd-body-color: var(--black-color);
|
--bg-motd-body-color: var(--black-color);
|
||||||
--bg-blocklist-hover-color: var(--black-color);
|
--bg-blocklist-hover-color: var(--black-color);
|
||||||
--bg-blocklist-item-selected-color: var(--black-color);
|
--bg-blocklist-item-selected-color: var(--black-color);
|
||||||
--bg-input-group-addon-color: var(--grey-3);
|
--bg-input-group-addon-color: var(--grey-3);
|
||||||
--bg-table-color: var(--black-color);
|
--bg-table-color: var(--black-color);
|
||||||
--bg-codemirror-gutters-color: var(--ui-gray-warm-11);
|
|
||||||
--bg-codemirror-color: var(--black-color);
|
|
||||||
--bg-codemirror-selected-color: var(--grey-3);
|
|
||||||
--bg-log-viewer-color: var(--black-color);
|
--bg-log-viewer-color: var(--black-color);
|
||||||
--bg-log-line-selected-color: var(--grey-3);
|
--bg-log-line-selected-color: var(--grey-3);
|
||||||
--bg-modal-content-color: var(--black-color);
|
--bg-modal-content-color: var(--black-color);
|
||||||
|
@ -536,7 +517,6 @@
|
||||||
--text-tooltip-color: var(--white-color);
|
--text-tooltip-color: var(--white-color);
|
||||||
--text-blocklist-item-selected-color: var(--blue-9);
|
--text-blocklist-item-selected-color: var(--blue-9);
|
||||||
--text-input-group-addon-color: var(--white-color);
|
--text-input-group-addon-color: var(--white-color);
|
||||||
--text-codemirror-color: var(--white-color);
|
|
||||||
--text-dropdown-menu-color: var(--white-color);
|
--text-dropdown-menu-color: var(--white-color);
|
||||||
--text-log-viewer-color: var(--white-color);
|
--text-log-viewer-color: var(--white-color);
|
||||||
--text-summary-color: var(--white-color);
|
--text-summary-color: var(--white-color);
|
||||||
|
@ -582,7 +562,6 @@
|
||||||
--border-pre-next-month: var(--white-color);
|
--border-pre-next-month: var(--white-color);
|
||||||
--border-daterangepicker-after: var(--black-color);
|
--border-daterangepicker-after: var(--black-color);
|
||||||
--border-pre-color: var(--grey-3);
|
--border-pre-color: var(--grey-3);
|
||||||
--border-codemirror-cursor-color: var(--white-color);
|
|
||||||
--border-modal: 1px solid var(--white-color);
|
--border-modal: 1px solid var(--white-color);
|
||||||
--border-sortbutton: var(--black-color);
|
--border-sortbutton: var(--black-color);
|
||||||
--border-bootbox: var(--black-color);
|
--border-bootbox: var(--black-color);
|
||||||
|
@ -596,9 +575,7 @@
|
||||||
|
|
||||||
--text-input-textarea: var(--black-color);
|
--text-input-textarea: var(--black-color);
|
||||||
--bg-item-highlighted-null-color: var(--grey-2);
|
--bg-item-highlighted-null-color: var(--grey-2);
|
||||||
--text-cm-default-color: var(--blue-9);
|
|
||||||
--text-cm-meta-color: var(--white-color);
|
|
||||||
--text-cm-string-color: var(--red-7);
|
|
||||||
--text-progress-bar-color: var(--black-color);
|
--text-progress-bar-color: var(--black-color);
|
||||||
|
|
||||||
--user-menu-icon-color: var(--white-color);
|
--user-menu-icon-color: var(--white-color);
|
||||||
|
|
|
@ -36,6 +36,10 @@
|
||||||
border: 1px solid var(--border-input-group-addon-color);
|
border: 1px solid var(--border-input-group-addon-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group .form-control {
|
||||||
|
z-index: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.text-danger {
|
.text-danger {
|
||||||
color: var(--ui-error-9);
|
color: var(--ui-error-9);
|
||||||
}
|
}
|
||||||
|
@ -150,50 +154,6 @@ code {
|
||||||
background-color: var(--bg-table-selected-color);
|
background-color: var(--bg-table-selected-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background: var(--bg-codemirror-gutters-color);
|
|
||||||
border-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-linenumber {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror pre.CodeMirror-line,
|
|
||||||
.CodeMirror pre.CodeMirror-line-like {
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
background: var(--bg-codemirror-color);
|
|
||||||
color: var(--text-codemirror-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-selected {
|
|
||||||
background: var(--bg-codemirror-selected-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-cursor {
|
|
||||||
border-left: 1px solid var(--border-codemirror-cursor-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-default .cm-atom {
|
|
||||||
color: var(--text-cm-default-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-default .cm-meta {
|
|
||||||
color: var(--text-cm-meta-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-default .cm-string {
|
|
||||||
color: var(--text-cm-string-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-default .cm-number {
|
|
||||||
color: var(--text-cm-number-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background: var(--bg-dropdown-menu-color);
|
background: var(--bg-dropdown-menu-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -358,11 +318,6 @@ input:-webkit-autofill {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overide Vendor CSS */
|
/* Overide Vendor CSS */
|
||||||
|
|
||||||
.btn-link:hover {
|
|
||||||
color: var(--text-link-hover-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiSelect.inlineBlock button {
|
.multiSelect.inlineBlock button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.7 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.2 KiB |
|
@ -1,5 +1,7 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||||
|
|
||||||
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { reactModule } from './react';
|
import { reactModule } from './react';
|
||||||
|
@ -16,14 +18,17 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
abstract: true,
|
abstract: true,
|
||||||
onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService) {
|
onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
if (![1, 2, 4].includes(endpoint.Type)) {
|
const dockerTypes = [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment];
|
||||||
|
|
||||||
|
if (!dockerTypes.includes(endpoint.Type)) {
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await checkEndpointStatus(endpoint);
|
const status = await checkEndpointStatus(endpoint);
|
||||||
|
|
||||||
if (endpoint.Type !== 4) {
|
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
|
||||||
await updateEndpointStatus(endpoint, status);
|
await updateEndpointStatus(endpoint, status);
|
||||||
}
|
}
|
||||||
endpoint.Status = status;
|
endpoint.Status = status;
|
||||||
|
@ -34,16 +39,22 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
|
|
||||||
await StateManager.updateEndpointState(endpoint);
|
await StateManager.updateEndpointState(endpoint);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Notifications.error('Failed loading environment', e);
|
let params = {};
|
||||||
$state.go('portainer.home', {}, { reload: true });
|
|
||||||
|
if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
|
||||||
|
params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'docker.dashboard' };
|
||||||
|
} else {
|
||||||
|
Notifications.error('Failed loading environment', e);
|
||||||
|
}
|
||||||
|
$state.go('portainer.home', params, { reload: true, inherit: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkEndpointStatus(endpoint) {
|
async function checkEndpointStatus(endpoint) {
|
||||||
try {
|
try {
|
||||||
await SystemService.ping(endpoint.Id);
|
await SystemService.ping(endpoint.Id);
|
||||||
return 1;
|
return EnvironmentStatus.Up;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 2;
|
return EnvironmentStatus.Down;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- use registry -->
|
<!-- use registry -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group" ng-if="$ctrl.model.UseRegistry">
|
<div class="form-group" ng-if="$ctrl.model.UseRegistry">
|
||||||
<label for="image_registry" class="control-label col-sm-3 col-lg-2 required text-left" ng-class="$ctrl.labelClass"> Registry </label>
|
<label for="image_registry" class="control-label col-sm-3 col-lg-2 text-left" ng-class="$ctrl.labelClass"> Registry </label>
|
||||||
<div ng-class="$ctrl.inputClass" class="col-sm-8">
|
<div ng-class="$ctrl.inputClass" class="col-sm-8">
|
||||||
<select
|
<select
|
||||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
title="Search image on Docker Hub"
|
title="Search image on Docker Hub"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<pr-icon icon="'svg-docker'" size="'lg'"></pr-icon> Search
|
<pr-icon icon="'svg-docker'" size="'md'"></pr-icon> Search
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||||
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||||
|
@ -11,6 +12,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
||||||
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||||
|
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||||
|
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
|
||||||
|
import { InsightsBox } from '@/react/components/InsightsBox';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.docker.react.components', [])
|
.module('portainer.docker.react.components', [])
|
||||||
|
@ -37,5 +41,28 @@ export const componentsModule = angular
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'gpu',
|
'gpu',
|
||||||
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
r2a(Gpu, [
|
||||||
).name;
|
'values',
|
||||||
|
'onChange',
|
||||||
|
'gpus',
|
||||||
|
'usedGpus',
|
||||||
|
'usedAllGpus',
|
||||||
|
'enableGpuManagement',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'gpusList',
|
||||||
|
r2a(withControlledInput(GpusList), ['value', 'onChange'])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'insightsBox',
|
||||||
|
r2a(InsightsBox, [
|
||||||
|
'header',
|
||||||
|
'content',
|
||||||
|
'setHtmlContent',
|
||||||
|
'insightCloseId',
|
||||||
|
'type',
|
||||||
|
'className',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.component('gpusInsights', r2a(GpusInsights, [])).name;
|
||||||
|
|
|
@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'$timeout',
|
'$timeout',
|
||||||
'$transition$',
|
'$transition$',
|
||||||
'$filter',
|
'$filter',
|
||||||
|
'$analytics',
|
||||||
'Container',
|
'Container',
|
||||||
'ContainerHelper',
|
'ContainerHelper',
|
||||||
'Image',
|
'Image',
|
||||||
|
@ -35,6 +36,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'FormValidator',
|
'FormValidator',
|
||||||
'RegistryService',
|
'RegistryService',
|
||||||
'SystemService',
|
'SystemService',
|
||||||
|
'SettingsService',
|
||||||
'PluginService',
|
'PluginService',
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'endpoint',
|
'endpoint',
|
||||||
|
@ -46,6 +48,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$timeout,
|
$timeout,
|
||||||
$transition$,
|
$transition$,
|
||||||
$filter,
|
$filter,
|
||||||
|
$analytics,
|
||||||
Container,
|
Container,
|
||||||
ContainerHelper,
|
ContainerHelper,
|
||||||
Image,
|
Image,
|
||||||
|
@ -60,6 +63,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
FormValidator,
|
FormValidator,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
SystemService,
|
SystemService,
|
||||||
|
SettingsService,
|
||||||
PluginService,
|
PluginService,
|
||||||
HttpRequestHelper,
|
HttpRequestHelper,
|
||||||
endpoint
|
endpoint
|
||||||
|
@ -1042,6 +1046,18 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendAnalytics() {
|
||||||
|
const publicSettings = await SettingsService.publicSettings();
|
||||||
|
const analyticsAllowed = publicSettings.EnableTelemetry;
|
||||||
|
const image = `${$scope.formValues.RegistryModel.Registry.URL}/${$scope.formValues.RegistryModel.Image}`;
|
||||||
|
if (analyticsAllowed && $scope.formValues.GPU.enabled) {
|
||||||
|
$analytics.eventTrack('gpuContainerCreated', {
|
||||||
|
category: 'docker',
|
||||||
|
metadata: { gpu: $scope.formValues.GPU, containerImage: image },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyResourceControl(newContainer) {
|
function applyResourceControl(newContainer) {
|
||||||
const userId = Authentication.getUserDetails().ID;
|
const userId = Authentication.getUserDetails().ID;
|
||||||
const resourceControl = newContainer.Portainer.ResourceControl;
|
const resourceControl = newContainer.Portainer.ResourceControl;
|
||||||
|
@ -1101,7 +1117,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
return validateForm(accessControlData, $scope.isAdmin);
|
return validateForm(accessControlData, $scope.isAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSuccess() {
|
async function onSuccess() {
|
||||||
|
await sendAnalytics();
|
||||||
Notifications.success('Success', 'Container successfully created');
|
Notifications.success('Success', 'Container successfully created');
|
||||||
$state.go('docker.containers', {}, { reload: true });
|
$state.go('docker.containers', {}, { reload: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<form class="form-horizontal" autocomplete="off">
|
<form class="form-horizontal" autocomplete="off">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,8 +37,6 @@
|
||||||
model="formValues.RegistryModel"
|
model="formValues.RegistryModel"
|
||||||
ng-if="formValues.RegistryModel.Registry"
|
ng-if="formValues.RegistryModel.Registry"
|
||||||
auto-complete="true"
|
auto-complete="true"
|
||||||
label-class="col-sm-1"
|
|
||||||
input-class="col-sm-11"
|
|
||||||
endpoint="endpoint"
|
endpoint="endpoint"
|
||||||
is-admin="isAdmin"
|
is-admin="isAdmin"
|
||||||
check-rate-limits="formValues.alwaysPull"
|
check-rate-limits="formValues.alwaysPull"
|
||||||
|
@ -169,7 +167,7 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm !ml-0"
|
||||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)
|
||||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||||
ng-click="create()"
|
ng-click="create()"
|
||||||
|
@ -701,17 +699,20 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !shm-size-input -->
|
<!-- !shm-size-input -->
|
||||||
<!-- #region GPU -->
|
<!-- #region GPU -->
|
||||||
<div class="col-sm-12 form-section-title"> GPU </div>
|
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||||
|
<div class="col-sm-12 form-section-title"> GPU </div>
|
||||||
|
|
||||||
<gpu
|
<gpu
|
||||||
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||||
values="formValues.GPU"
|
values="formValues.GPU"
|
||||||
on-change="(onGpuChange)"
|
on-change="(onGpuChange)"
|
||||||
gpus="endpoint.Gpus"
|
gpus="endpoint.Gpus"
|
||||||
used-gpus="gpuUseList"
|
used-gpus="gpuUseList"
|
||||||
used-all-gpus="gpuUseAll"
|
used-all-gpus="gpuUseAll"
|
||||||
>
|
enable-gpu-management="endpoint.EnableGPUManagement"
|
||||||
</gpu>
|
>
|
||||||
|
</gpu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- #endregion GPU -->
|
<!-- #endregion GPU -->
|
||||||
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-grid mx-4">
|
<div class="mx-4 grid grid-cols-2 gap-3">
|
||||||
<a class="no-link" ui-sref="docker.stacks" ng-if="showStacks">
|
<a class="no-link" ui-sref="docker.stacks" ng-if="showStacks">
|
||||||
<dashboard-item icon="'layers'" type="'Stack'" value="stackCount"></dashboard-item>
|
<dashboard-item icon="'layers'" type="'Stack'" value="stackCount"></dashboard-item>
|
||||||
</a>
|
</a>
|
||||||
|
@ -106,7 +106,12 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<dashboard-item icon="'cpu'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
|
<dashboard-item
|
||||||
|
ng-if="endpoint.EnableGPUManagement && applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'"
|
||||||
|
icon="'cpu'"
|
||||||
|
type="'GPU'"
|
||||||
|
value="endpoint.Gpus.length"
|
||||||
|
></dashboard-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,10 +2,13 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
export default class DockerFeaturesConfigurationController {
|
export default class DockerFeaturesConfigurationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $scope, EndpointService, Notifications, StateManager) {
|
constructor($async, $scope, $state, $analytics, EndpointService, SettingsService, Notifications, StateManager) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$scope = $scope;
|
this.$scope = $scope;
|
||||||
|
this.$state = $state;
|
||||||
|
this.$analytics = $analytics;
|
||||||
this.EndpointService = EndpointService;
|
this.EndpointService = EndpointService;
|
||||||
|
this.SettingsService = SettingsService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.StateManager = StateManager;
|
this.StateManager = StateManager;
|
||||||
|
|
||||||
|
@ -35,6 +38,8 @@ export default class DockerFeaturesConfigurationController {
|
||||||
this.save = this.save.bind(this);
|
this.save = this.save.bind(this);
|
||||||
this.onChangeField = this.onChangeField.bind(this);
|
this.onChangeField = this.onChangeField.bind(this);
|
||||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||||
|
this.onToggleGPUManagement = this.onToggleGPUManagement.bind(this);
|
||||||
|
this.onGpusChange = this.onGpusChange.bind(this);
|
||||||
this.onChangeEnableHostManagementFeatures = this.onChangeField('enableHostManagementFeatures');
|
this.onChangeEnableHostManagementFeatures = this.onChangeField('enableHostManagementFeatures');
|
||||||
this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
||||||
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
||||||
|
@ -52,6 +57,12 @@ export default class DockerFeaturesConfigurationController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleGPUManagement(checked) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.state.enableGPUManagement = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onChange(values) {
|
onChange(values) {
|
||||||
return this.$scope.$evalAsync(() => {
|
return this.$scope.$evalAsync(() => {
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
|
@ -69,6 +80,12 @@ export default class DockerFeaturesConfigurationController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onGpusChange(value) {
|
||||||
|
return this.$async(async () => {
|
||||||
|
this.endpoint.Gpus = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isContainerEditDisabled() {
|
isContainerEditDisabled() {
|
||||||
const {
|
const {
|
||||||
disableBindMountsForRegularUsers,
|
disableBindMountsForRegularUsers,
|
||||||
|
@ -92,7 +109,11 @@ export default class DockerFeaturesConfigurationController {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
const securitySettings = {
|
|
||||||
|
const validGpus = this.endpoint.Gpus.filter((gpu) => gpu.name && gpu.value);
|
||||||
|
const gpus = this.state.enableGPUManagement ? validGpus : [];
|
||||||
|
|
||||||
|
const settings = {
|
||||||
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
|
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
|
||||||
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
||||||
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
||||||
|
@ -102,33 +123,53 @@ export default class DockerFeaturesConfigurationController {
|
||||||
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
||||||
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
||||||
allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers,
|
allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers,
|
||||||
|
enableGPUManagement: this.state.enableGPUManagement,
|
||||||
|
gpus,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
|
const publicSettings = await this.SettingsService.publicSettings();
|
||||||
|
const analyticsAllowed = publicSettings.EnableTelemetry;
|
||||||
|
if (analyticsAllowed) {
|
||||||
|
// send analytics if GPU management is changed (with the new state)
|
||||||
|
if (this.initialEnableGPUManagement !== this.state.enableGPUManagement) {
|
||||||
|
this.$analytics.eventTrack('enable-gpu-management-updated', { category: 'portainer', metadata: { enableGPUManagementState: this.state.enableGPUManagement } });
|
||||||
|
}
|
||||||
|
// send analytics if the number of GPUs is changed (with a list of the names)
|
||||||
|
if (gpus.length > this.initialGPUs.length) {
|
||||||
|
const numberOfGPUSAdded = this.endpoint.Gpus.length - this.initialGPUs.length;
|
||||||
|
this.$analytics.eventTrack('gpus-added', { category: 'portainer', metadata: { gpus: gpus.map((gpu) => gpu.name), numberOfGPUSAdded } });
|
||||||
|
}
|
||||||
|
if (gpus.length < this.initialGPUs.length) {
|
||||||
|
const numberOfGPUSRemoved = this.initialGPUs.length - this.endpoint.Gpus.length;
|
||||||
|
this.$analytics.eventTrack('gpus-removed', { category: 'portainer', metadata: { gpus: gpus.map((gpu) => gpu.name), numberOfGPUSRemoved } });
|
||||||
|
}
|
||||||
|
this.initialGPUs = gpus;
|
||||||
|
this.initialEnableGPUManagement = this.state.enableGPUManagement;
|
||||||
|
}
|
||||||
|
|
||||||
this.endpoint.SecuritySettings = securitySettings;
|
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, settings);
|
||||||
|
|
||||||
|
this.endpoint.SecuritySettings = settings;
|
||||||
this.Notifications.success('Success', 'Saved settings successfully');
|
this.Notifications.success('Success', 'Saved settings successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.Notifications.error('Failure', e, 'Failed saving settings');
|
this.Notifications.error('Failure', e, 'Failed saving settings');
|
||||||
}
|
}
|
||||||
this.state.actionInProgress = false;
|
this.state.actionInProgress = false;
|
||||||
|
this.$state.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAgent() {
|
|
||||||
const applicationState = this.StateManager.getState();
|
|
||||||
return applicationState.endpoint.mode.agentProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
const securitySettings = this.endpoint.SecuritySettings;
|
const securitySettings = this.endpoint.SecuritySettings;
|
||||||
|
|
||||||
const isAgent = this.checkAgent();
|
const applicationState = this.StateManager.getState();
|
||||||
this.isAgent = isAgent;
|
this.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||||
|
|
||||||
|
this.isDockerStandaloneEnv = applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE';
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
|
enableHostManagementFeatures: this.isAgent && securitySettings.enableHostManagementFeatures,
|
||||||
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
allowVolumeBrowserForRegularUsers: this.isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
||||||
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
||||||
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
||||||
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
||||||
|
@ -137,5 +178,11 @@ export default class DockerFeaturesConfigurationController {
|
||||||
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
||||||
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
|
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this.endpoint.Gpus could be null as it is Gpus: []Pair in the API
|
||||||
|
this.endpoint.Gpus = this.endpoint.Gpus || [];
|
||||||
|
this.state.enableGPUManagement = this.isDockerStandaloneEnv && (this.endpoint.EnableGPUManagement || this.endpoint.Gpus.length > 0);
|
||||||
|
this.initialGPUs = this.endpoint.Gpus;
|
||||||
|
this.initialEnableGPUManagement = this.endpoint.EnableGPUManagement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,9 +150,28 @@
|
||||||
<!-- other -->
|
<!-- other -->
|
||||||
<div class="col-sm-12 form-section-title"> Other </div>
|
<div class="col-sm-12 form-section-title"> Other </div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12 pb-3">
|
||||||
|
<gpus-insights></gpus-insights>
|
||||||
|
</div>
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
label="'Show a notification to indicate out-of-date images for Docker environments'"
|
label="'Show GPU in the UI'"
|
||||||
|
tooltip="'This allows managing of GPUs for container/stack hardware acceleration via the Portainer UI.'"
|
||||||
|
checked="$ctrl.state.enableGPUManagement"
|
||||||
|
name="'enableGPUManagement'"
|
||||||
|
on-change="($ctrl.onToggleGPUManagement)"
|
||||||
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
disabled="!$ctrl.isDockerStandaloneEnv"
|
||||||
|
></por-switch-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="pl-4">
|
||||||
|
<gpus-list ng-if="$ctrl.state.enableGPUManagement && $ctrl.endpoint" value="$ctrl.endpoint.Gpus" on-change="($ctrl.onGpusChange)"></gpus-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<por-switch-field
|
||||||
|
label="'Show an image(s) up to date indicator for Stacks, Services and Containers'"
|
||||||
checked="false"
|
checked="false"
|
||||||
name="'outOfDateImageToggle'"
|
name="'outOfDateImageToggle'"
|
||||||
label-class="'col-sm-7 col-lg-4'"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
@ -166,7 +185,13 @@
|
||||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm !ml-0"
|
||||||
|
ng-click="$ctrl.save()"
|
||||||
|
ng-disabled="$ctrl.state.actionInProgress"
|
||||||
|
button-spinner="$ctrl.state.actionInProgress"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
|
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -167,6 +167,6 @@ function confirmImageForceRemoval() {
|
||||||
title: 'Are you sure?',
|
title: 'Are you sure?',
|
||||||
modalType: ModalType.Destructive,
|
modalType: ModalType.Destructive,
|
||||||
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
|
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
|
||||||
confirmButton: buildConfirmButton('Remote the image', 'danger'),
|
confirmButton: buildConfirmButton('Remove the image', 'danger'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,5 @@
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === undefined">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge group
|
|
||||||
selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
|
||||||
</p>
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === $ctrl.EditorType.Compose && $ctrl.hasKubeEndpoint()">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Edge groups with kubernetes environments no longer support compose deployment types in Portainer. Please select
|
|
||||||
edge groups that only have docker environments when using compose deployment types.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<edge-stack-deployment-type-selector
|
<edge-stack-deployment-type-selector
|
||||||
allow-kube-to-select-compose="$ctrl.allowKubeToSelectCompose"
|
allow-kube-to-select-compose="$ctrl.allowKubeToSelectCompose"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form class="form-horizontal" name="EdgeGroupForm" ng-submit="$ctrl.formAction()">
|
<form class="form-horizontal" name="EdgeGroupForm" ng-submit="$ctrl.handleSubmit()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="group_name" class="col-sm-3 col-lg-2 control-label required text-left"> Name </label>
|
<label for="group_name" class="col-sm-3 col-lg-2 control-label required text-left"> Name </label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
|
|
@ -37,6 +37,7 @@ export class EdgeGroupFormController {
|
||||||
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
||||||
this.onChangeModel = this.onChangeModel.bind(this);
|
this.onChangeModel = this.onChangeModel.bind(this);
|
||||||
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
$scope.$watch(
|
$scope.$watch(
|
||||||
() => this.model,
|
() => this.model,
|
||||||
|
@ -118,6 +119,10 @@ export class EdgeGroupFormController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
this.formAction(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
this.getTags();
|
this.getTags();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,19 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
|
||||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||||
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.edge.react.components', [])
|
.module('portainer.edge.react.components', [])
|
||||||
.component(
|
.component(
|
||||||
'edgeGroupsSelector',
|
'edgeGroupsSelector',
|
||||||
r2a(EdgeGroupsSelector, ['items', 'onChange', 'value'])
|
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
||||||
|
'onChange',
|
||||||
|
'value',
|
||||||
|
'error',
|
||||||
|
'horizontal',
|
||||||
|
'isGroupVisible',
|
||||||
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'edgeScriptForm',
|
'edgeScriptForm',
|
||||||
|
@ -21,6 +28,7 @@ export const componentsModule = angular
|
||||||
'commands',
|
'commands',
|
||||||
'isNomadTokenVisible',
|
'isNomadTokenVisible',
|
||||||
'asyncMode',
|
'asyncMode',
|
||||||
|
'showMetaFields',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -21,7 +21,6 @@ export class CreateEdgeGroupController {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createGroup = this.createGroup.bind(this);
|
this.createGroup = this.createGroup.bind(this);
|
||||||
this.createGroupAsync = this.createGroupAsync.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
|
@ -31,20 +30,18 @@ export class CreateEdgeGroupController {
|
||||||
this.state.loaded = true;
|
this.state.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
createGroup() {
|
async createGroup(model) {
|
||||||
return this.$async(this.createGroupAsync);
|
return this.$async(async () => {
|
||||||
}
|
this.state.actionInProgress = true;
|
||||||
|
try {
|
||||||
async createGroupAsync() {
|
await this.EdgeGroupService.create(model);
|
||||||
this.state.actionInProgress = true;
|
this.Notifications.success('Success', 'Edge group successfully created');
|
||||||
try {
|
this.$state.go('edge.groups');
|
||||||
await this.EdgeGroupService.create(this.model);
|
} catch (err) {
|
||||||
this.Notifications.success('Success', 'Edge group successfully created');
|
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
||||||
this.$state.go('edge.groups');
|
} finally {
|
||||||
} catch (err) {
|
this.state.actionInProgress = false;
|
||||||
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
}
|
||||||
} finally {
|
});
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ export class EditEdgeGroupController {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateGroup = this.updateGroup.bind(this);
|
this.updateGroup = this.updateGroup.bind(this);
|
||||||
this.updateGroupAsync = this.updateGroupAsync.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
|
@ -28,20 +27,18 @@ export class EditEdgeGroupController {
|
||||||
this.state.loaded = true;
|
this.state.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroup() {
|
updateGroup(group) {
|
||||||
return this.$async(this.updateGroupAsync);
|
return this.$async(async () => {
|
||||||
}
|
this.state.actionInProgress = true;
|
||||||
|
try {
|
||||||
async updateGroupAsync() {
|
await this.EdgeGroupService.update(group);
|
||||||
this.state.actionInProgress = true;
|
this.Notifications.success('Success', 'Edge group successfully updated');
|
||||||
try {
|
this.$state.go('edge.groups');
|
||||||
await this.EdgeGroupService.update(this.model);
|
} catch (err) {
|
||||||
this.Notifications.success('Success', 'Edge group successfully updated');
|
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
||||||
this.$state.go('edge.groups');
|
} finally {
|
||||||
} catch (err) {
|
this.state.actionInProgress = false;
|
||||||
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
}
|
||||||
} finally {
|
});
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,6 @@ export default class CreateEdgeStackViewController {
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
try {
|
try {
|
||||||
this.edgeGroups = await this.EdgeGroupService.groups();
|
this.edgeGroups = await this.EdgeGroupService.groups();
|
||||||
this.noGroups = this.edgeGroups.length === 0;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,19 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
<edge-groups-selector value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||||
<div class="form-group" ng-if="$ctrl.edgeGroups">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
|
||||||
</div>
|
|
||||||
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
|
|
||||||
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
|
|
||||||
</div>
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.formValues.DeploymentType === undefined">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge
|
|
||||||
group selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<edge-stack-deployment-type-selector
|
<edge-stack-deployment-type-selector
|
||||||
value="$ctrl.formValues.DeploymentType"
|
value="$ctrl.formValues.DeploymentType"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||||
|
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/service';
|
||||||
|
|
||||||
|
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||||
|
|
||||||
import registriesModule from './registries';
|
import registriesModule from './registries';
|
||||||
import customTemplateModule from './custom-templates';
|
import customTemplateModule from './custom-templates';
|
||||||
import { reactModule } from './react';
|
import { reactModule } from './react';
|
||||||
|
@ -16,31 +21,49 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
|
|
||||||
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) {
|
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
if (![5, 6, 7].includes(endpoint.Type)) {
|
const kubeTypes = [
|
||||||
|
PortainerEndpointTypes.KubernetesLocalEnvironment,
|
||||||
|
PortainerEndpointTypes.AgentOnKubernetesEnvironment,
|
||||||
|
PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!kubeTypes.includes(endpoint.Type)) {
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (endpoint.Type === 7) {
|
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||||
//edge
|
//edge
|
||||||
try {
|
try {
|
||||||
await KubernetesHealthService.ping(endpoint.Id);
|
await KubernetesHealthService.ping(endpoint.Id);
|
||||||
endpoint.Status = 1;
|
endpoint.Status = EnvironmentStatus.Up;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
endpoint.Status = 2;
|
endpoint.Status = EnvironmentStatus.Down;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await StateManager.updateEndpointState(endpoint);
|
await StateManager.updateEndpointState(endpoint);
|
||||||
|
|
||||||
if (endpoint.Type === 7 && endpoint.Status === 2) {
|
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
|
||||||
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
|
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await KubernetesNamespaceService.get();
|
// use selfsubject access review to check if we can connect to the kubernetes environment
|
||||||
|
// because it's gets a fast response, and is accessible to all users
|
||||||
|
try {
|
||||||
|
await getSelfSubjectAccessReview(endpoint.Id, 'default');
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Environment is unreachable.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Notifications.error('Failed loading environment', e);
|
let params = {};
|
||||||
$state.go('portainer.home', {}, { reload: true });
|
|
||||||
|
if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||||
|
params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'kubernetes.dashboard' };
|
||||||
|
} else {
|
||||||
|
Notifications.error('Failed loading environment', e);
|
||||||
|
}
|
||||||
|
$state.go('portainer.home', params, { reload: true, inherit: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,116 +1,144 @@
|
||||||
<div class="datatable">
|
<div class="datatable">
|
||||||
<!-- toolbar header actions and settings -->
|
<!-- toolbar header actions and settings -->
|
||||||
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col gap-1">
|
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col !gap-0">
|
||||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||||
<div class="toolBarTitle vertical-center">
|
<div class="toolBarTitle vertical-center">
|
||||||
<div class="widget-icon space-right">
|
<div class="widget-icon space-right">
|
||||||
<pr-icon icon="'box'"></pr-icon>
|
<pr-icon icon="'box'"></pr-icon>
|
||||||
</div>
|
</div>
|
||||||
Applications
|
Applications
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar vertical-center !mr-0 min-w-[280px]">
|
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||||
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||||
<input
|
<div class="settings" data-cy="k8sApp-tableSettings">
|
||||||
type="text"
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
class="searchInput"
|
<span uib-dropdown-toggle aria-label="Settings">
|
||||||
ng-model="$ctrl.state.textFilter"
|
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
</span>
|
||||||
placeholder="Search for an application..."
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
auto-focus
|
<div class="tableMenu">
|
||||||
ng-model-options="{ debounce: 300 }"
|
<div class="menuHeader"> Table settings </div>
|
||||||
data-cy="k8sApp-searchApplicationsInput"
|
<div class="menuContent">
|
||||||
/>
|
<div>
|
||||||
</div>
|
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||||
<div class="actionBar !mr-0 !gap-3">
|
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||||
<button
|
<label for="applications_setting_show_system">Show system resources</label>
|
||||||
ng-if="$ctrl.isPrimary"
|
</div>
|
||||||
type="button"
|
<div class="md-checkbox">
|
||||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
<input
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
id="setting_auto_refresh"
|
||||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
type="checkbox"
|
||||||
data-cy="k8sApp-removeAppButton"
|
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||||
>
|
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||||
<pr-icon icon="'trash-2'"></pr-icon>
|
data-cy="k8sApp-autoRefreshCheckbox"
|
||||||
Remove
|
/>
|
||||||
</button>
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
<button
|
</div>
|
||||||
ng-if="$ctrl.isPrimary"
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
type="button"
|
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||||
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
|
<select
|
||||||
ui-sref="kubernetes.applications.new"
|
id="settings_refresh_rate"
|
||||||
data-cy="k8sApp-addApplicationButton"
|
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||||
>
|
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
class="small-select"
|
||||||
</button>
|
data-cy="k8sApp-refreshRateDropdown"
|
||||||
<button
|
>
|
||||||
ng-if="$ctrl.isPrimary"
|
<option value="10">10s</option>
|
||||||
type="button"
|
<option value="30">30s</option>
|
||||||
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
|
<option value="60">1min</option>
|
||||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.applications' })"
|
<option value="120">2min</option>
|
||||||
data-cy="k8sApp-deployFromManifestButton"
|
<option value="300">5min</option>
|
||||||
>
|
</select>
|
||||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
<span>
|
||||||
</button>
|
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||||
</div>
|
</span>
|
||||||
<div class="settings" data-cy="k8sApp-tableSettings">
|
</div>
|
||||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
|
||||||
<span uib-dropdown-toggle aria-label="Settings">
|
|
||||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
|
||||||
</span>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
|
||||||
<div class="tableMenu">
|
|
||||||
<div class="menuHeader"> Table settings </div>
|
|
||||||
<div class="menuContent">
|
|
||||||
<div>
|
|
||||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
|
||||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
|
||||||
<label for="applications_setting_show_system">Show system resources</label>
|
|
||||||
</div>
|
|
||||||
<div class="md-checkbox">
|
|
||||||
<input
|
|
||||||
id="setting_auto_refresh"
|
|
||||||
type="checkbox"
|
|
||||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
|
||||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
|
||||||
data-cy="k8sApp-autoRefreshCheckbox"
|
|
||||||
/>
|
|
||||||
<label for="setting_auto_refresh">Auto refresh</label>
|
|
||||||
</div>
|
|
||||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
|
||||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
|
||||||
<select
|
|
||||||
id="settings_refresh_rate"
|
|
||||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
|
||||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
|
||||||
class="small-select"
|
|
||||||
data-cy="k8sApp-refreshRateDropdown"
|
|
||||||
>
|
|
||||||
<option value="10">10s</option>
|
|
||||||
<option value="30">30s</option>
|
|
||||||
<option value="60">1min</option>
|
|
||||||
<option value="120">2min</option>
|
|
||||||
<option value="300">5min</option>
|
|
||||||
</select>
|
|
||||||
<span>
|
|
||||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actionBar !mr-0 !gap-3">
|
||||||
|
<button
|
||||||
|
ng-if="$ctrl.isPrimary"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||||
|
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||||
|
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||||
|
data-cy="k8sApp-removeAppButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'trash-2'"></pr-icon>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ng-if="$ctrl.isPrimary"
|
||||||
|
hide-deployment-option="form"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
|
||||||
|
ui-sref="kubernetes.applications.new"
|
||||||
|
data-cy="k8sApp-addApplicationButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ng-if="$ctrl.isPrimary"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
|
||||||
|
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.applications' })"
|
||||||
|
data-cy="k8sApp-deployFromManifestButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="searchBar">
|
||||||
|
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
placeholder="Search..."
|
||||||
|
auto-focus
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
data-cy="k8sApp-searchApplicationsInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] min-w-[140px]">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon">
|
||||||
|
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
|
||||||
|
Namespace
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
class="form-control !h-[30px] !py-1"
|
||||||
|
ng-model="$ctrl.state.namespace"
|
||||||
|
ng-change="$ctrl.onChangeNamespace()"
|
||||||
|
data-cy="component-namespaceSelect"
|
||||||
|
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||||
|
>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||||
<span class="small text-muted vertical-center mt-1">
|
<span class="small text-muted vertical-center mt-1">
|
||||||
<pr-icon icon="'info'" mode="'primary'" class="vertical-center"></pr-icon>
|
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||||
System resources are hidden, this can be changed in the table settings.
|
System resources are hidden, this can be changed in the table settings.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="w-fit">
|
||||||
|
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- data table content -->
|
<!-- data table content -->
|
||||||
<div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
|
<div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
|
||||||
|
|
|
@ -15,5 +15,10 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
|
||||||
refreshCallback: '<',
|
refreshCallback: '<',
|
||||||
onPublishingModeClick: '<',
|
onPublishingModeClick: '<',
|
||||||
isPrimary: '<',
|
isPrimary: '<',
|
||||||
|
namespaces: '<',
|
||||||
|
namespace: '<',
|
||||||
|
onChangeNamespaceDropdown: '<',
|
||||||
|
isSystemResources: '<',
|
||||||
|
setSystemResources: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,8 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||||
this.state = Object.assign(this.state, {
|
this.state = Object.assign(this.state, {
|
||||||
expandAll: false,
|
expandAll: false,
|
||||||
expandedItems: [],
|
expandedItems: [],
|
||||||
|
namespace: '',
|
||||||
|
namespaces: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
|
@ -70,6 +72,8 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onSettingsShowSystemChange = function () {
|
this.onSettingsShowSystemChange = function () {
|
||||||
|
this.updateNamespace();
|
||||||
|
this.setSystemResources(this.settings.showSystem);
|
||||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,6 +139,45 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||||
this.filters.state.values = _.uniqBy(availableTypeFilters, 'type');
|
this.filters.state.values = _.uniqBy(availableTypeFilters, 'type');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onChangeNamespace = function () {
|
||||||
|
this.onChangeNamespaceDropdown(this.state.namespace);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateNamespace = function () {
|
||||||
|
if (this.namespaces && this.settingsLoaded) {
|
||||||
|
const allNamespacesOption = { Name: 'All namespaces', Value: '', IsSystem: false };
|
||||||
|
const visibleNamespaceOptions = this.namespaces
|
||||||
|
.filter((ns) => {
|
||||||
|
if (!this.settings.showSystem && ns.IsSystem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((ns) => ({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem }));
|
||||||
|
this.state.namespaces = [allNamespacesOption, ...visibleNamespaceOptions];
|
||||||
|
|
||||||
|
if (this.state.namespace && !this.state.namespaces.find((ns) => ns.Name === this.state.namespace)) {
|
||||||
|
if (this.state.namespaces.length > 1) {
|
||||||
|
let defaultNS = this.state.namespaces.find((ns) => ns.Name === 'default');
|
||||||
|
defaultNS = defaultNS || this.state.namespaces[1];
|
||||||
|
this.state.namespace = defaultNS.Value;
|
||||||
|
} else {
|
||||||
|
this.state.namespace = this.state.namespaces[0].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onChanges = function () {
|
||||||
|
if (typeof this.isSystemResources !== 'undefined') {
|
||||||
|
this.settings.showSystem = this.isSystemResources;
|
||||||
|
DatatableService.setDataTableSettings(this.settingsKey, this.settings);
|
||||||
|
}
|
||||||
|
this.state.namespace = this.namespace;
|
||||||
|
this.updateNamespace();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
};
|
||||||
|
|
||||||
this.$onInit = function () {
|
this.$onInit = function () {
|
||||||
this.isAdmin = Authentication.isAdmin();
|
this.isAdmin = Authentication.isAdmin();
|
||||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||||
|
@ -172,7 +215,16 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||||
if (storedSettings !== null) {
|
if (storedSettings !== null) {
|
||||||
this.settings = storedSettings;
|
this.settings = storedSettings;
|
||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
|
|
||||||
|
this.setSystemResources && this.setSystemResources(this.settings.showSystem);
|
||||||
}
|
}
|
||||||
|
this.settingsLoaded = true;
|
||||||
|
// Set the default selected namespace
|
||||||
|
if (!this.state.namespace) {
|
||||||
|
this.state.namespace = this.namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateNamespace();
|
||||||
this.onSettingsRepeaterChange();
|
this.onSettingsRepeaterChange();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,99 +1,122 @@
|
||||||
<div class="datatable">
|
<div class="datatable">
|
||||||
<!-- table title and action menu -->
|
<!-- table title and action menu -->
|
||||||
<div class="toolBar !flex-col gap-1">
|
<div class="toolBar !flex-col !gap-0">
|
||||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||||
<!-- title -->
|
|
||||||
<div class="toolBarTitle vertical-center">
|
<div class="toolBarTitle vertical-center">
|
||||||
<div class="widget-icon space-right">
|
<div class="widget-icon space-right">
|
||||||
<pr-icon icon="'list'"></pr-icon>
|
<pr-icon icon="'list'"></pr-icon>
|
||||||
</div>
|
</div>
|
||||||
Stacks
|
Stacks
|
||||||
</div>
|
</div>
|
||||||
<!-- actions -->
|
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||||
<div class="searchBar vertical-center">
|
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||||
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
|
<div class="actionBar !mr-0 !gap-3">
|
||||||
<input
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
class="searchInput min-w-min self-start"
|
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||||
ng-model="$ctrl.state.textFilter"
|
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||||
placeholder="Search for a stack..."
|
data-cy="k8sApp-removeStackButton"
|
||||||
auto-focus
|
>
|
||||||
ng-model-options="{ debounce: 300 }"
|
<pr-icon icon="'trash-2'"></pr-icon>
|
||||||
/>
|
Remove
|
||||||
</div>
|
</button>
|
||||||
<div class="actionBar !mr-0 !gap-3">
|
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
||||||
<button
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
type="button"
|
<span uib-dropdown-toggle>
|
||||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
</span>
|
||||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
data-cy="k8sApp-removeStackButton"
|
<div class="tableMenu">
|
||||||
>
|
<div class="menuHeader"> Table settings </div>
|
||||||
<pr-icon icon="'trash-2'"></pr-icon>
|
<div class="menuContent">
|
||||||
Remove
|
<div>
|
||||||
</button>
|
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||||
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
<label for="applications_setting_show_system">Show system resources</label>
|
||||||
<span uib-dropdown-toggle>
|
</div>
|
||||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
<div class="md-checkbox">
|
||||||
</span>
|
<input
|
||||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
id="setting_auto_refresh"
|
||||||
<div class="tableMenu">
|
type="checkbox"
|
||||||
<div class="menuHeader"> Table settings </div>
|
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||||
<div class="menuContent">
|
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||||
<div>
|
data-cy="k8sApp-autoRefreshCheckbox-stack"
|
||||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
/>
|
||||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
<label for="applications_setting_show_system">Show system resources</label>
|
</div>
|
||||||
</div>
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
<div class="md-checkbox">
|
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||||
<input
|
<select
|
||||||
id="setting_auto_refresh"
|
id="settings_refresh_rate"
|
||||||
type="checkbox"
|
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
class="small-select"
|
||||||
data-cy="k8sApp-autoRefreshCheckbox-stack"
|
data-cy="k8sApp-refreshRateDropdown-stack"
|
||||||
/>
|
>
|
||||||
<label for="setting_auto_refresh">Auto refresh</label>
|
<option value="10">10s</option>
|
||||||
</div>
|
<option value="30">30s</option>
|
||||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
<option value="60">1min</option>
|
||||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
<option value="120">2min</option>
|
||||||
<select
|
<option value="300">5min</option>
|
||||||
id="settings_refresh_rate"
|
</select>
|
||||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
<span>
|
||||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||||
class="small-select"
|
</span>
|
||||||
data-cy="k8sApp-refreshRateDropdown-stack"
|
</div>
|
||||||
>
|
|
||||||
<option value="10">10s</option>
|
|
||||||
<option value="30">30s</option>
|
|
||||||
<option value="60">1min</option>
|
|
||||||
<option value="120">2min</option>
|
|
||||||
<option value="300">5min</option>
|
|
||||||
</select>
|
|
||||||
<span>
|
|
||||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
|
||||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar vertical-center">
|
||||||
|
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput min-w-min self-start"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
placeholder="Search..."
|
||||||
|
auto-focus
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] w-fit min-w-[140px]">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon">
|
||||||
|
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
|
||||||
|
Namespace
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
class="form-control !h-[30px] !py-1"
|
||||||
|
ng-model="$ctrl.state.namespace"
|
||||||
|
ng-change="$ctrl.onChangeNamespace()"
|
||||||
|
data-cy="component-namespaceSelect"
|
||||||
|
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||||
|
>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- info text -->
|
<!-- info text -->
|
||||||
<div class="flex w-full flex-row">
|
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||||
<span class="small text-muted vertical-center mt-1" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
<span class="small text-muted vertical-center mt-1">
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||||
System resources are hidden, this can be changed in the table settings.
|
System resources are hidden, this can be changed in the table settings.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="w-fit">
|
||||||
|
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table-hover nowrap-cells table">
|
<table class="table-hover nowrap-cells table">
|
||||||
|
|
|
@ -10,5 +10,10 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDa
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
refreshCallback: '<',
|
refreshCallback: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
|
namespaces: '<',
|
||||||
|
namespace: '<',
|
||||||
|
onChangeNamespaceDropdown: '<',
|
||||||
|
isSystemResources: '<',
|
||||||
|
setSystemResources: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,8 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
||||||
this.state = Object.assign(this.state, {
|
this.state = Object.assign(this.state, {
|
||||||
expandedItems: [],
|
expandedItems: [],
|
||||||
expandAll: false,
|
expandAll: false,
|
||||||
|
namespace: '',
|
||||||
|
namespaces: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
@ -22,6 +24,8 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onSettingsShowSystemChange = function () {
|
this.onSettingsShowSystemChange = function () {
|
||||||
|
this.updateNamespace();
|
||||||
|
this.setSystemResources(this.settings.showSystem);
|
||||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,6 +80,44 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onChangeNamespace = function () {
|
||||||
|
this.onChangeNamespaceDropdown(this.state.namespace);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateNamespace = function () {
|
||||||
|
if (this.namespaces) {
|
||||||
|
const namespaces = [{ Name: 'All namespaces', Value: '', IsSystem: false }];
|
||||||
|
this.namespaces.find((ns) => {
|
||||||
|
if (!this.settings.showSystem && ns.IsSystem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
namespaces.push({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem });
|
||||||
|
});
|
||||||
|
this.state.namespaces = namespaces;
|
||||||
|
|
||||||
|
if (this.state.namespace && !this.state.namespaces.find((ns) => ns.Name === this.state.namespace)) {
|
||||||
|
if (this.state.namespaces.length > 1) {
|
||||||
|
let defaultNS = this.state.namespaces.find((ns) => ns.Name === 'default');
|
||||||
|
defaultNS = defaultNS || this.state.namespaces[1];
|
||||||
|
this.state.namespace = defaultNS.Value;
|
||||||
|
} else {
|
||||||
|
this.state.namespace = this.state.namespaces[0].Value;
|
||||||
|
}
|
||||||
|
this.onChangeNamespaceDropdown(this.state.namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onChanges = function () {
|
||||||
|
if (typeof this.isSystemResources !== 'undefined') {
|
||||||
|
this.settings.showSystem = this.isSystemResources;
|
||||||
|
DatatableService.setDataTableSettings(this.settingsKey, this.settings);
|
||||||
|
}
|
||||||
|
this.state.namespace = this.namespace;
|
||||||
|
this.updateNamespace();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
};
|
||||||
|
|
||||||
this.$onInit = function () {
|
this.$onInit = function () {
|
||||||
this.isAdmin = Authentication.isAdmin();
|
this.isAdmin = Authentication.isAdmin();
|
||||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||||
|
@ -103,11 +145,20 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
||||||
this.filters.state.open = false;
|
this.filters.state.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
var storedSettings = DatatableService.getDataTableSettings(this.settingsKey);
|
||||||
if (storedSettings !== null) {
|
if (storedSettings !== null) {
|
||||||
this.settings = storedSettings;
|
this.settings = storedSettings;
|
||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
|
|
||||||
|
this.setSystemResources && this.setSystemResources(this.settings.showSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the default selected namespace
|
||||||
|
if (!this.state.namespace) {
|
||||||
|
this.state.namespace = this.namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateNamespace();
|
||||||
this.onSettingsRepeaterChange();
|
this.onSettingsRepeaterChange();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -141,7 +141,9 @@ export default class HelmTemplatesController {
|
||||||
try {
|
try {
|
||||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||||
|
|
||||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
const nonSystemNamespaces = resourcePools.filter(
|
||||||
|
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||||
|
);
|
||||||
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||||
this.state.resourcePool = this.state.resourcePools[0];
|
this.state.resourcePool = this.state.resourcePools[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
placeholder="# Define or paste the content of your manifest here"
|
placeholder="# Define or paste the content of your manifest here"
|
||||||
read-only="true"
|
read-only="true"
|
||||||
hide-title="true"
|
hide-title="true"
|
||||||
|
height="{{ $ctrl.expanded ? '800px' : '500px' }}"
|
||||||
>
|
>
|
||||||
</web-editor-form>
|
</web-editor-form>
|
||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
|
|
|
@ -33,9 +33,6 @@ class KubernetesYamlInspectorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleYAMLInspectorExpansion() {
|
toggleYAMLInspectorExpansion() {
|
||||||
let selector = 'kubernetes-yaml-inspector code-editor > div.CodeMirror';
|
|
||||||
let height = this.expanded ? '500px' : '80vh';
|
|
||||||
$(selector).css({ height: height });
|
|
||||||
this.expanded = !this.expanded;
|
this.expanded = !this.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable';
|
import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable';
|
||||||
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
|
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
|
||||||
|
import { DashboardView } from '@/react/kubernetes/DashboardView';
|
||||||
import { ServicesView } from '@/react/kubernetes/ServicesView';
|
import { ServicesView } from '@/react/kubernetes/ServicesView';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
|
@ -24,4 +25,8 @@ export const viewsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'kubernetesIngressesCreateView',
|
'kubernetesIngressesCreateView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesDashboardView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -7,9 +7,10 @@ import $allSettled from 'Portainer/services/allSettled';
|
||||||
|
|
||||||
class KubernetesNamespaceService {
|
class KubernetesNamespaceService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, KubernetesNamespaces) {
|
constructor($async, KubernetesNamespaces, LocalStorage) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.KubernetesNamespaces = KubernetesNamespaces;
|
this.KubernetesNamespaces = KubernetesNamespaces;
|
||||||
|
this.LocalStorage = LocalStorage;
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
this.getAllAsync = this.getAllAsync.bind(this);
|
this.getAllAsync = this.getAllAsync.bind(this);
|
||||||
|
@ -17,6 +18,7 @@ class KubernetesNamespaceService {
|
||||||
this.deleteAsync = this.deleteAsync.bind(this);
|
this.deleteAsync = this.deleteAsync.bind(this);
|
||||||
this.getJSONAsync = this.getJSONAsync.bind(this);
|
this.getJSONAsync = this.getJSONAsync.bind(this);
|
||||||
this.updateFinalizeAsync = this.updateFinalizeAsync.bind(this);
|
this.updateFinalizeAsync = this.updateFinalizeAsync.bind(this);
|
||||||
|
this.refreshCacheAsync = this.refreshCacheAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,11 +81,20 @@ class KubernetesNamespaceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name) {
|
async get(name, refreshCache = false) {
|
||||||
if (name) {
|
if (name) {
|
||||||
return this.$async(this.getAsync, name);
|
return this.$async(this.getAsync, name);
|
||||||
}
|
}
|
||||||
return this.$async(this.getAllAsync);
|
const cachedAllowedNamespaces = this.LocalStorage.getAllowedNamespaces();
|
||||||
|
if (!cachedAllowedNamespaces || refreshCache) {
|
||||||
|
const allowedNamespaces = await this.getAllAsync();
|
||||||
|
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
|
||||||
|
updateNamespaces(allowedNamespaces);
|
||||||
|
return allowedNamespaces;
|
||||||
|
} else {
|
||||||
|
updateNamespaces(cachedAllowedNamespaces);
|
||||||
|
return cachedAllowedNamespaces;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +105,7 @@ class KubernetesNamespaceService {
|
||||||
const payload = KubernetesNamespaceConverter.createPayload(namespace);
|
const payload = KubernetesNamespaceConverter.createPayload(namespace);
|
||||||
const params = {};
|
const params = {};
|
||||||
const data = await this.KubernetesNamespaces().create(params, payload).$promise;
|
const data = await this.KubernetesNamespaces().create(params, payload).$promise;
|
||||||
|
await this.refreshCacheAsync();
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new PortainerError('Unable to create namespace', err);
|
throw new PortainerError('Unable to create namespace', err);
|
||||||
|
@ -104,6 +116,14 @@ class KubernetesNamespaceService {
|
||||||
return this.$async(this.createAsync, namespace);
|
return this.$async(this.createAsync, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshCacheAsync() {
|
||||||
|
this.LocalStorage.deleteAllowedNamespaces();
|
||||||
|
const allowedNamespaces = await this.getAllAsync();
|
||||||
|
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
|
||||||
|
updateNamespaces(allowedNamespaces);
|
||||||
|
return allowedNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE
|
* DELETE
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -36,8 +36,8 @@ export function KubernetesResourcePoolService(
|
||||||
}
|
}
|
||||||
|
|
||||||
// getting the quota for all namespaces is costly by default, so disable getting it by default
|
// getting the quota for all namespaces is costly by default, so disable getting it by default
|
||||||
async function getAll({ getQuota = false }) {
|
async function getAll({ getQuota = false, refreshCache = false }) {
|
||||||
const namespaces = await KubernetesNamespaceService.get();
|
const namespaces = await KubernetesNamespaceService.get('', refreshCache);
|
||||||
const pools = await Promise.all(
|
const pools = await Promise.all(
|
||||||
_.map(namespaces, async (namespace) => {
|
_.map(namespaces, async (namespace) => {
|
||||||
const name = namespace.Name;
|
const name = namespace.Name;
|
||||||
|
|
|
@ -19,6 +19,11 @@
|
||||||
refresh-callback="ctrl.getApplications"
|
refresh-callback="ctrl.getApplications"
|
||||||
on-publishing-mode-click="(ctrl.onPublishingModeClick)"
|
on-publishing-mode-click="(ctrl.onPublishingModeClick)"
|
||||||
is-primary="true"
|
is-primary="true"
|
||||||
|
namespaces="ctrl.state.namespaces"
|
||||||
|
namespace="ctrl.state.namespaceName"
|
||||||
|
on-change-namespace-dropdown="(ctrl.onChangeNamespaceDropdown)"
|
||||||
|
is-system-resources="ctrl.state.isSystemResources"
|
||||||
|
set-system-resources="(ctrl.setSystemResources)"
|
||||||
>
|
>
|
||||||
</kubernetes-applications-datatable>
|
</kubernetes-applications-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
@ -30,6 +35,11 @@
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
refresh-callback="ctrl.getApplications"
|
refresh-callback="ctrl.getApplications"
|
||||||
remove-action="ctrl.removeStacksAction"
|
remove-action="ctrl.removeStacksAction"
|
||||||
|
namespaces="ctrl.state.namespaces"
|
||||||
|
namespace="ctrl.state.namespaceName"
|
||||||
|
on-change-namespace-dropdown="(ctrl.onChangeNamespaceDropdown)"
|
||||||
|
is-system-resources="ctrl.state.isSystemResources"
|
||||||
|
set-system-resources="(ctrl.setSystemResources)"
|
||||||
>
|
>
|
||||||
</kubernetes-applications-stacks-datatable>
|
</kubernetes-applications-stacks-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
|
@ -9,9 +9,23 @@ import { confirmDelete } from '@@/modals/confirm';
|
||||||
|
|
||||||
class KubernetesApplicationsController {
|
class KubernetesApplicationsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, KubernetesApplicationService, HelmService, KubernetesConfigurationService, Authentication, LocalStorage, StackService) {
|
constructor(
|
||||||
|
$async,
|
||||||
|
$state,
|
||||||
|
$scope,
|
||||||
|
Authentication,
|
||||||
|
Notifications,
|
||||||
|
KubernetesApplicationService,
|
||||||
|
HelmService,
|
||||||
|
KubernetesConfigurationService,
|
||||||
|
LocalStorage,
|
||||||
|
StackService,
|
||||||
|
KubernetesNamespaceService
|
||||||
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.Authentication = Authentication;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
this.HelmService = HelmService;
|
this.HelmService = HelmService;
|
||||||
|
@ -19,6 +33,7 @@ class KubernetesApplicationsController {
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
this.StackService = StackService;
|
this.StackService = StackService;
|
||||||
|
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.getApplications = this.getApplications.bind(this);
|
this.getApplications = this.getApplications.bind(this);
|
||||||
|
@ -28,6 +43,8 @@ class KubernetesApplicationsController {
|
||||||
this.removeStacksAction = this.removeStacksAction.bind(this);
|
this.removeStacksAction = this.removeStacksAction.bind(this);
|
||||||
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
||||||
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
||||||
|
this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
|
||||||
|
this.setSystemResources = this.setSystemResources.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab(index) {
|
selectTab(index) {
|
||||||
|
@ -126,20 +143,36 @@ class KubernetesApplicationsController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeNamespaceDropdown(namespaceName) {
|
||||||
|
this.state.namespaceName = namespaceName;
|
||||||
|
// save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
|
||||||
|
this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
|
||||||
|
this.getApplicationsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
async getApplicationsAsync() {
|
async getApplicationsAsync() {
|
||||||
try {
|
try {
|
||||||
const [applications, configurations] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesConfigurationService.get()]);
|
const [applications, configurations] = await Promise.all([
|
||||||
|
this.KubernetesApplicationService.get(this.state.namespaceName),
|
||||||
|
this.KubernetesConfigurationService.get(this.state.namespaceName),
|
||||||
|
]);
|
||||||
const configuredApplications = KubernetesConfigurationHelper.getApplicationConfigurations(applications, configurations);
|
const configuredApplications = KubernetesConfigurationHelper.getApplicationConfigurations(applications, configurations);
|
||||||
const { helmApplications, nonHelmApplications } = KubernetesApplicationHelper.getNestedApplications(configuredApplications);
|
const { helmApplications, nonHelmApplications } = KubernetesApplicationHelper.getNestedApplications(configuredApplications);
|
||||||
|
|
||||||
this.state.applications = [...helmApplications, ...nonHelmApplications];
|
this.state.applications = [...helmApplications, ...nonHelmApplications];
|
||||||
this.state.stacks = KubernetesStackHelper.stacksFromApplications(applications);
|
this.state.stacks = KubernetesStackHelper.stacksFromApplications(applications);
|
||||||
this.state.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications);
|
this.state.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications);
|
||||||
|
|
||||||
|
this.$scope.$apply();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSystemResources(flag) {
|
||||||
|
this.state.isSystemResources = flag;
|
||||||
|
}
|
||||||
|
|
||||||
getApplications() {
|
getApplications() {
|
||||||
return this.$async(this.getApplicationsAsync);
|
return this.$async(this.getApplicationsAsync);
|
||||||
}
|
}
|
||||||
|
@ -153,7 +186,27 @@ class KubernetesApplicationsController {
|
||||||
applications: [],
|
applications: [],
|
||||||
stacks: [],
|
stacks: [],
|
||||||
ports: [],
|
ports: [],
|
||||||
|
namespaces: [],
|
||||||
|
namespaceName: '',
|
||||||
|
isSystemResources: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.user = this.Authentication.getUserDetails();
|
||||||
|
this.state.namespaces = await this.KubernetesNamespaceService.get();
|
||||||
|
|
||||||
|
const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
|
||||||
|
const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
|
||||||
|
|
||||||
|
this.state.namespaces = this.state.namespaces.filter((n) => n.Status === 'Active');
|
||||||
|
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
|
||||||
|
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
|
||||||
|
if (!this.state.namespaces.length || preferredNamespace === '') {
|
||||||
|
this.state.namespaceName = '';
|
||||||
|
} else {
|
||||||
|
// otherwise, set the preferred namespaceName if it exists, otherwise set the first namespaceName
|
||||||
|
this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
|
||||||
|
}
|
||||||
|
|
||||||
await this.getApplications();
|
await this.getApplications();
|
||||||
|
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
|
|
|
@ -212,7 +212,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #end region IMAGE FIELD -->
|
<!-- #end region IMAGE FIELD -->
|
||||||
|
|
||||||
<div class="col-sm-12 !p-0">
|
<div class="col-sm-12 mb-4 !p-0">
|
||||||
<annotations-be-teaser></annotations-be-teaser>
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 mt-4">
|
<div class="col-sm-12 mt-2">
|
||||||
<span
|
<span
|
||||||
ng-if="ctrl.formValues.Containers.length <= 1"
|
ng-if="ctrl.formValues.Containers.length <= 1"
|
||||||
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
||||||
|
@ -503,7 +503,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||||
<div class="col-sm-12 vertical-center pt-2.5" style="margin-top: 5px" ng-if="!ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
|
<div class="col-sm-12 vertical-center mb-2 pt-2.5" style="margin-top: 5px" ng-if="!ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
|
||||||
<label class="control-label !pt-0 text-left">Persisted folders</label>
|
<label class="control-label !pt-0 text-left">Persisted folders</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1208,22 +1208,31 @@ class KubernetesCreateApplicationController {
|
||||||
]);
|
]);
|
||||||
this.nodesLimits = nodesLimits;
|
this.nodesLimits = nodesLimits;
|
||||||
|
|
||||||
const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
const nonSystemNamespaces = _.filter(
|
||||||
|
resourcePools,
|
||||||
|
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||||
|
);
|
||||||
|
|
||||||
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
|
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
|
||||||
this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||||
|
|
||||||
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
|
// this.state.nodes.memory and this.state.nodes.cpu are used to calculate the slider limits, so set them before calling updateSliders()
|
||||||
this.formValues.ResourcePool = this.resourcePools[0];
|
|
||||||
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
|
|
||||||
if (!this.formValues.ResourcePool) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_.forEach(nodes, (item) => {
|
_.forEach(nodes, (item) => {
|
||||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||||
this.state.nodes.cpu += item.CPU;
|
this.state.nodes.cpu += item.CPU;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.resourcePools.length) {
|
||||||
|
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
|
||||||
|
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
|
||||||
|
this.updateNamespaceLimits(namespaceWithQuota);
|
||||||
|
this.updateSliders(namespaceWithQuota);
|
||||||
|
}
|
||||||
|
this.formValues.ResourcePool = this.resourcePools[0];
|
||||||
|
if (!this.formValues.ResourcePool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
||||||
this.nodeNumber = nodes.length;
|
this.nodeNumber = nodes.length;
|
||||||
|
|
||||||
|
@ -1281,9 +1290,6 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;
|
this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;
|
||||||
|
|
||||||
this.oldFormValues = angular.copy(this.formValues);
|
this.oldFormValues = angular.copy(this.formValues);
|
||||||
|
|
||||||
this.updateNamespaceLimits(namespaceWithQuota);
|
|
||||||
this.updateSliders(namespaceWithQuota);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||||
<!-- resource-pool -->
|
<!-- resource-pool -->
|
||||||
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
|
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
|
||||||
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label required text-left">Namespace</label>
|
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
|
||||||
<div class="col-sm-8 col-lg-9">
|
<div class="col-sm-8 col-lg-9">
|
||||||
<select
|
<select
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
|
|
@ -196,7 +196,10 @@ class KubernetesCreateConfigurationController {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
this.resourcePools = _.filter(
|
||||||
|
resourcePools,
|
||||||
|
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||||
|
);
|
||||||
|
|
||||||
this.formValues.ResourcePool = this.resourcePools[0];
|
this.formValues.ResourcePool = this.resourcePools[0];
|
||||||
await this.getConfigurations();
|
await this.getConfigurations();
|
||||||
|
|
|
@ -165,7 +165,10 @@ class KubernetesConfigureController {
|
||||||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||||
const resourcePools = _.filter(
|
const resourcePools = _.filter(
|
||||||
allResourcePools,
|
allResourcePools,
|
||||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
|
(resourcePool) =>
|
||||||
|
!KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) &&
|
||||||
|
!KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) &&
|
||||||
|
resourcePool.Namespace.Status === 'Active'
|
||||||
);
|
);
|
||||||
|
|
||||||
ingressesToDel.forEach((ingress) => {
|
ingressesToDel.forEach((ingress) => {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue