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>.
|
||||
|
||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
|
||||
|
||||
### 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_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
|
||||
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -42,7 +43,9 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
|||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*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.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
|
||||
displayDeprecationWarnings(flags)
|
||||
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
|
@ -111,31 +111,38 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
|||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
if endpointURL != "" {
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
if endpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
return err
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval != "" {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
if snapshotInterval == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -12,13 +12,14 @@ func Confirm(message string) (bool, error) {
|
|||
fmt.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
|
||||
answer = strings.ReplaceAll(answer, "\n", "")
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
// FIXME: In 2.16 we changed the way ingress controller permissions are
|
||||
// stored. Instead of being stored as annotation on an ingress rule, we keep
|
||||
// them in our database. However, in order to run the migration we need an
|
||||
// admin kube client to run lookup the old ingress rules and compare them
|
||||
// with the current existing ingress classes.
|
||||
//
|
||||
// Unfortunately, our migrations run as part of the database initialization
|
||||
// and our kubeclients require an initialized database. So it is not
|
||||
// possible to do this migration as part of our normal flow. We DO have a
|
||||
// migration which toggles a boolean in kubernetes configuration that
|
||||
// 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 {
|
||||
// Our normal migrations run as part of the database initialization
|
||||
// but some more complex migrations require access to a kubernetes or docker
|
||||
// client. Therefore we run a separate migration process just before
|
||||
// starting the server.
|
||||
postInitMigrator := datastore.NewPostInitMigrator(
|
||||
kubernetesClientFactory,
|
||||
dockerClientFactory,
|
||||
dataStore,
|
||||
)
|
||||
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
|
@ -115,9 +114,6 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
|||
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
|||
var object string
|
||||
err := conn.UnmarshalObject(test.object, &object)
|
||||
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 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 {
|
||||
|
|
|
@ -9,8 +9,7 @@ import (
|
|||
|
||||
// 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) {
|
||||
switch storeType {
|
||||
case "boltdb":
|
||||
if storeType == "boltdb" {
|
||||
return &boltdb.DbConnection{
|
||||
Path: storePath,
|
||||
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 (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -39,7 +44,7 @@ func (m *Migrator) updateEdgeStackStatusForDB90() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUserThemForDB90() error {
|
||||
func (m *Migrator) updateUserThemeForDB90() error {
|
||||
log.Info().Msg("updating existing user theme settings")
|
||||
|
||||
users, err := m.userService.Users()
|
||||
|
@ -60,3 +65,28 @@ func (m *Migrator) updateUserThemForDB90() error {
|
|||
|
||||
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,
|
||||
"EdgeKey": "",
|
||||
"EnableGPUManagement": false,
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Id": 1,
|
||||
|
@ -63,6 +64,7 @@
|
|||
"UseServerMetrics": false
|
||||
},
|
||||
"Flags": {
|
||||
"IsServerIngressClassDetected": false,
|
||||
"IsServerMetricsDetected": false,
|
||||
"IsServerStorageDetected": false
|
||||
},
|
||||
|
@ -71,6 +73,7 @@
|
|||
"LastCheckInDate": 0,
|
||||
"Name": "local",
|
||||
"PostInitMigrations": {
|
||||
"MigrateGPUs": true,
|
||||
"MigrateIngresses": true
|
||||
},
|
||||
"PublicURL": "",
|
||||
|
@ -903,8 +906,7 @@
|
|||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
|
@ -934,8 +936,7 @@
|
|||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
|
@ -943,6 +944,6 @@
|
|||
}
|
||||
],
|
||||
"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.
|
||||
// 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) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
return nil, errUnsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
case portainer.AgentOnDockerEnvironment:
|
||||
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)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
return createLocalClient(endpoint)
|
||||
}
|
||||
|
||||
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{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||
|
@ -75,6 +76,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
|||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -84,7 +86,9 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "logout")
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
|
@ -101,6 +105,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
|||
} else {
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth")
|
||||
}
|
||||
|
||||
if !pullImage {
|
||||
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 {
|
||||
env = append(env, envvar.Name+"="+envvar.Value)
|
||||
}
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
||||
}
|
||||
|
||||
|
@ -121,7 +127,9 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
|
@ -198,6 +206,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
|||
if config["HttpHeaders"] == nil {
|
||||
config["HttpHeaders"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
headersObject := config["HttpHeaders"].(map[string]interface{})
|
||||
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
|
||||
headersObject["X-PortainerAgent-Signature"] = signature
|
||||
|
@ -230,5 +239,6 @@ func configureFilePaths(args []string, filePaths []string) []string {
|
|||
for _, path := range filePaths {
|
||||
args = append(args, "--compose-file", path)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ func newHttpClientForAzure() *http.Client {
|
|||
}
|
||||
|
||||
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
||||
|
||||
return httpsCli
|
||||
}
|
||||
|
||||
|
@ -98,10 +99,12 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
|||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build download url")
|
||||
}
|
||||
|
||||
zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip")
|
||||
if err != nil {
|
||||
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 {
|
||||
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
|
||||
}
|
||||
|
||||
return zipFile.Name(), nil
|
||||
}
|
||||
|
||||
|
@ -141,6 +145,7 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
return nil, errors.Errorf("failed to get latest commitID in the repository")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
path := strings.Split(rawUrl, "/")
|
||||
|
@ -343,6 +349,7 @@ func (a *azureClient) buildTreeUrl(config *azureOptions, rootObjectHash string)
|
|||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
// projectId={projectId}&recursive=true&fileName={fileName}&$format={$format}&api-version=6.0
|
||||
q.Set("recursive", "true")
|
||||
|
@ -361,9 +368,11 @@ func formatReferenceName(name string) string {
|
|||
if strings.HasPrefix(name, branchPrefix) {
|
||||
return strings.TrimPrefix(name, branchPrefix)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, tagPrefix) {
|
||||
return strings.TrimPrefix(name, tagPrefix)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
|
@ -371,9 +380,11 @@ func getVersionType(name string) string {
|
|||
if strings.HasPrefix(name, branchPrefix) {
|
||||
return "branch"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, tagPrefix) {
|
||||
return "tag"
|
||||
}
|
||||
|
||||
return "commit"
|
||||
}
|
||||
|
||||
|
@ -490,5 +501,6 @@ func checkAzureStatusCode(err error, code int) error {
|
|||
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -8,14 +8,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
)
|
||||
const privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
|
||||
func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
|
@ -107,7 +106,7 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
|||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
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)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
|
@ -269,7 +268,7 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
|||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
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{})
|
||||
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")
|
||||
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
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
|
@ -224,7 +224,7 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
|||
repositoryUrl := privateGitRepoURL
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
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{})
|
||||
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 {
|
||||
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
|
||||
}
|
||||
|
||||
iter, err := repo.Log(&git.LogOptions{All: true})
|
||||
if err != nil {
|
||||
t.Fatalf("can't get a commit history iterator with error %v", err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
err = iter.ForEach(func(_ *object.Commit) error {
|
||||
count++
|
||||
|
@ -107,6 +109,7 @@ func getCommitHistoryLength(t *testing.T, err error, dir string) int {
|
|||
if err != nil {
|
||||
t.Fatalf("can't iterate over the commit history with error %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
REPOSITORY_CACHE_SIZE = 4
|
||||
REPOSITORY_CACHE_TTL = 5 * time.Minute
|
||||
const (
|
||||
repositoryCacheSize = 4
|
||||
repositoryCacheTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
// 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.
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
|
||||
hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
|
||||
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
||||
if !hashChanged && !forceUpdate {
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
@ -132,7 +134,9 @@ func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
@ -151,7 +155,9 @@ func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||
|
@ -182,7 +188,9 @@ func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
@ -201,7 +209,9 @@ func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
@ -225,7 +235,9 @@ func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
|
|
@ -33,10 +33,13 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return "", errorResponse
|
||||
|
|
|
@ -128,6 +128,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code %s", response.Status)
|
||||
|
@ -137,6 +138,8 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certificate)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
||||
}
|
||||
|
|
|
@ -103,6 +103,8 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
|
@ -132,6 +134,8 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
|
@ -141,10 +145,12 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
|||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unexpected status code %s", response.Status)
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
|||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
|
@ -89,6 +90,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
|||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
|
|
|
@ -99,6 +99,8 @@ func backup(t *testing.T, h *Handler, password string) []byte {
|
|||
|
||||
response := w.Result()
|
||||
archive, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
return archive
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
@ -26,12 +25,15 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
|||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("invalid Edge group name")
|
||||
}
|
||||
|
||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
|
||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||
return errors.New("environment is mandatory for a static Edge group")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -56,7 +58,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
|
@ -101,13 +102,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
return txResponse(w, edgeGroup, err)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
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/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
|
@ -27,12 +27,15 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
|||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("invalid Edge group name")
|
||||
}
|
||||
|
||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
|
||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||
return errors.New("environments is mandatory for a static Edge group")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -62,128 +65,135 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
|||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if handler.DataStore.IsErrObjectNotFound(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"))
|
||||
}
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = tx.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if handler.DataStore.IsErrObjectNotFound(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)
|
||||
}
|
||||
|
||||
edgeGroup.Name = payload.Name
|
||||
}
|
||||
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 payload.Name != "" {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
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) {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
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
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
if payload.PartialMatch != nil {
|
||||
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)
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
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 {
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
|
||||
var operation string
|
||||
if slices.Contains(newRelatedEndpoints, endpointID) {
|
||||
operation = "add"
|
||||
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
||||
operation = "remove"
|
||||
edgeGroup.Dynamic = payload.Dynamic
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
} 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 {
|
||||
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 {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) error {
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -197,7 +207,7 @@ func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) er
|
|||
|
||||
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 {
|
||||
|
|
|
@ -3,11 +3,13 @@ package edgegroups
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
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))
|
||||
if err != nil {
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||
// EE-5910
|
||||
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
|
|
|
@ -30,7 +30,7 @@ var endpointTestCases = []endpointTestCase{
|
|||
{
|
||||
portainer.Endpoint{},
|
||||
portainer.EndpointRelation{},
|
||||
http.StatusNotFound,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
|
@ -43,7 +43,7 @@ var endpointTestCases = []endpointTestCase{
|
|||
portainer.EndpointRelation{
|
||||
EndpointID: -1,
|
||||
},
|
||||
http.StatusNotFound,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
|
|
|
@ -38,7 +38,6 @@ type endpointCreatePayload struct {
|
|||
AzureAuthenticationKey string
|
||||
TagIDs []portainer.TagID
|
||||
EdgeCheckinInterval int
|
||||
IsEdgeDevice bool
|
||||
}
|
||||
|
||||
type endpointCreationEnum int
|
||||
|
@ -381,7 +380,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
|||
EdgeKey: edgeKey,
|
||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
UserTrusted: true,
|
||||
}
|
||||
|
||||
|
@ -435,7 +433,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
|||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
err := handler.snapshotAndPersistEndpoint(endpoint)
|
||||
|
@ -501,7 +498,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
|||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -128,7 +129,6 @@ func getDockerHubToken(httpClient *client.HTTPClient, registry *portainer.Regist
|
|||
}
|
||||
|
||||
func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) {
|
||||
|
||||
requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
||||
|
@ -142,7 +142,9 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed fetching dockerhub limits")
|
||||
|
|
|
@ -28,6 +28,10 @@ type endpointSettingsUpdatePayload struct {
|
|||
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
|
||||
// Whether host management features are enabled
|
||||
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 {
|
||||
|
@ -107,6 +111,14 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
|
|||
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
if payload.EnableGPUManagement != nil {
|
||||
endpoint.EnableGPUManagement = *payload.EnableGPUManagement
|
||||
}
|
||||
|
||||
if payload.Gpus != nil {
|
||||
endpoint.Gpus = payload.Gpus
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||
|
|
|
@ -82,7 +82,7 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.18.0
|
||||
// @version 2.19.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
|
|
@ -48,15 +48,16 @@ func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *h
|
|||
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "editor":
|
||||
if method == "editor" {
|
||||
return handler.createFDOProfileFromFileContent(w, r)
|
||||
}
|
||||
|
||||
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 {
|
||||
var payload createProfileFromFileContentPayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
|
@ -66,6 +67,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
|||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
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")}
|
||||
}
|
||||
|
@ -80,6 +82,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
|||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
||||
}
|
||||
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
@ -10,7 +9,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"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 := ""
|
||||
repositoryPassword := ""
|
||||
if payload.RepositoryAuthentication {
|
||||
|
|
|
@ -10,10 +10,9 @@ import (
|
|||
)
|
||||
|
||||
type systemInfoResponse struct {
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
EdgeDevices int `json:"edgeDevices"`
|
||||
Agents int `json:"agents"`
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
Agents int `json:"agents"`
|
||||
}
|
||||
|
||||
// @id systemInfo
|
||||
|
@ -34,7 +33,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
|||
|
||||
agents := 0
|
||||
edgeAgents := 0
|
||||
edgeDevices := 0
|
||||
|
||||
for _, environment := range environments {
|
||||
if endpointutils.IsAgentEndpoint(&environment) {
|
||||
|
@ -45,9 +43,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
|||
edgeAgents++
|
||||
}
|
||||
|
||||
if environment.IsEdgeDevice {
|
||||
edgeDevices++
|
||||
}
|
||||
}
|
||||
|
||||
platform, err := plf.DetermineContainerPlatform()
|
||||
|
@ -56,9 +51,8 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
|||
}
|
||||
|
||||
return response.JSON(w, &systemInfoResponse{
|
||||
EdgeAgents: edgeAgents,
|
||||
EdgeDevices: edgeDevices,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
EdgeAgents: edgeAgents,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
)
|
||||
|
||||
type systemUpgradePayload struct {
|
||||
|
@ -28,13 +30,19 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
|
||||
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
|
||||
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
|
||||
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
|
||||
}
|
||||
|
||||
// @id systemUpgrade
|
||||
// @summary Upgrade Portainer to BE
|
||||
// @description Upgrade Portainer to BE
|
||||
// @description **Access policy**: administrator
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @success 204 {object} status "Success"
|
||||
// @router /system/upgrade [post]
|
||||
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return httperror.InternalServerError("Failed to upgrade Portainer", err)
|
||||
}
|
||||
|
||||
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 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]
|
||||
func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload filePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("HTTP error: unable to cleanup stack creation")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import (
|
|||
type themePayload struct {
|
||||
// Color represents the color theme of the UI
|
||||
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 {
|
||||
|
@ -33,11 +31,11 @@ type userUpdatePayload struct {
|
|||
|
||||
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
@ -120,10 +118,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
|||
if payload.Theme.Color != nil {
|
||||
user.ThemeSettings.Color = *payload.Theme.Color
|
||||
}
|
||||
|
||||
if payload.Theme.SubtleUpgradeButton != nil {
|
||||
user.ThemeSettings.SubtleUpgradeButton = *payload.Theme.SubtleUpgradeButton
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Role != 0 {
|
||||
|
|
|
@ -14,48 +14,57 @@ func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer
|
|||
_, in, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
_, err = writer.Write(in)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
||||
out := make([]byte, readerBufferSize)
|
||||
|
||||
for {
|
||||
out := make([]byte, readerBufferSize)
|
||||
_, err := reader.Read(out)
|
||||
n, err := reader.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
processedOutput := validString(string(out[:]))
|
||||
processedOutput := validString(string(out[:n]))
|
||||
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validString(s string) string {
|
||||
if !utf8.ValidString(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)
|
||||
}
|
||||
s = string(v)
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
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)
|
||||
decoratedResourceData = append(decoratedResourceData, responseObject)
|
||||
}
|
||||
|
||||
return decoratedResourceData, nil
|
||||
}
|
||||
|
||||
|
@ -34,8 +35,10 @@ func (transport *Transport) applyPortainerContainer(resourceObject map[string]in
|
|||
if !ok {
|
||||
return resourceObject, nil
|
||||
}
|
||||
|
||||
if len(resourceId) >= 12 && resourceId[0:12] == portainerContainerId {
|
||||
resourceObject["IsPortainer"] = true
|
||||
}
|
||||
|
||||
return resourceObject, nil
|
||||
}
|
||||
|
|
|
@ -150,8 +150,7 @@ func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenMa
|
|||
func decorateAgentRequest(r *http.Request, dataStore dataservices.DataStore) error {
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
||||
if strings.HasPrefix(requestPath, "/dockerhub") {
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = true
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
err = endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
|
||||
return
|
||||
|
@ -158,6 +173,16 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
|||
}
|
||||
|
||||
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")
|
||||
err := storageDetect(endpoint, endpointService, factory)
|
||||
if err == nil {
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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"
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -36,19 +26,23 @@ const (
|
|||
)
|
||||
|
||||
type Service interface {
|
||||
Upgrade(licenseKey string) error
|
||||
Upgrade(environment *portainer.Endpoint, licenseKey string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
composeDeployer libstack.Deployer
|
||||
isUpdating bool
|
||||
platform platform.ContainerPlatform
|
||||
assetsPath string
|
||||
composeDeployer libstack.Deployer
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
|
||||
isUpdating bool
|
||||
platform platform.ContainerPlatform
|
||||
|
||||
assetsPath string
|
||||
}
|
||||
|
||||
func NewService(
|
||||
assetsPath string,
|
||||
composeDeployer libstack.Deployer,
|
||||
kubernetesClientFactory *cli.ClientFactory,
|
||||
) (Service, error) {
|
||||
platform, err := platform.DetermineContainerPlatform()
|
||||
if err != nil {
|
||||
|
@ -56,13 +50,14 @@ func NewService(
|
|||
}
|
||||
|
||||
return &service{
|
||||
assetsPath: assetsPath,
|
||||
composeDeployer: composeDeployer,
|
||||
platform: platform,
|
||||
assetsPath: assetsPath,
|
||||
composeDeployer: composeDeployer,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *service) Upgrade(licenseKey string) error {
|
||||
func (service *service) Upgrade(environment *portainer.Endpoint, licenseKey string) error {
|
||||
service.isUpdating = true
|
||||
|
||||
switch service.platform {
|
||||
|
@ -70,113 +65,9 @@ func (service *service) Upgrade(licenseKey string) error {
|
|||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone")
|
||||
case platform.PlatformDockerSwarm:
|
||||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm")
|
||||
// case platform.PlatformKubernetes:
|
||||
// case platform.PlatformPodman:
|
||||
// case platform.PlatformNomad:
|
||||
// default:
|
||||
case platform.PlatformKubernetes:
|
||||
return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion)
|
||||
}
|
||||
|
||||
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
|
||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||
kcl.lock.Lock()
|
||||
defer kcl.lock.Unlock()
|
||||
kcl.mu.Lock()
|
||||
defer kcl.mu.Unlock()
|
||||
|
||||
policies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
|
@ -42,6 +42,7 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
@ -40,7 +39,6 @@ func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConf
|
|||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
config := &ktypes.ConfigMap{
|
||||
|
|
|
@ -7,17 +7,20 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultKubeClientQPS = 30
|
||||
DefaultKubeClientBurst = 100
|
||||
)
|
||||
|
||||
type (
|
||||
// ClientFactory is used to create Kubernetes clients
|
||||
ClientFactory struct {
|
||||
|
@ -25,16 +28,17 @@ type (
|
|||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
instanceID string
|
||||
endpointClients cmap.ConcurrentMap
|
||||
endpointClients map[string]*KubeClient
|
||||
endpointProxyClients *cache.Cache
|
||||
AddrHTTPS string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// KubeClient represent a service used to execute Kubernetes operations
|
||||
KubeClient struct {
|
||||
cli kubernetes.Interface
|
||||
instanceID string
|
||||
lock *sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -53,7 +57,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
|||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
instanceID: instanceID,
|
||||
endpointClients: cmap.New(),
|
||||
endpointClients: make(map[string]*KubeClient),
|
||||
endpointProxyClients: cache.New(timeout, timeout),
|
||||
AddrHTTPS: addrHTTPS,
|
||||
}, nil
|
||||
|
@ -65,82 +69,87 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
|
|||
|
||||
// Remove the cached kube client so a new one can be created
|
||||
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.
|
||||
// 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))
|
||||
client, ok := factory.endpointClients.Get(key)
|
||||
client, ok := factory.endpointClients[key]
|
||||
if !ok {
|
||||
client, err := factory.createCachedAdminKubeClient(endpoint)
|
||||
var err error
|
||||
|
||||
client, err = factory.createCachedAdminKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factory.endpointClients.Set(key, client)
|
||||
return client, nil
|
||||
factory.endpointClients[key] = client
|
||||
}
|
||||
|
||||
return client.(portainer.KubeClient), nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
||||
// calling SetProxyKubeClient before first. It is normally, called the
|
||||
// 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)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return client.(portainer.KubeClient), true
|
||||
|
||||
return client.(*KubeClient), true
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
||||
// 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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliConfig, err := config.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliConfig.QPS = DefaultKubeClientQPS
|
||||
cliConfig.Burst = DefaultKubeClientBurst
|
||||
|
||||
cli, err := kubernetes.NewForConfig(cliConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubecli := &KubeClient{
|
||||
return &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
}, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubecli := &KubeClient{
|
||||
return &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateClient returns a pointer to a new Clientset instance
|
||||
|
@ -199,7 +208,10 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
|
@ -218,30 +230,13 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) PostInitMigrateIngresses() 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 {
|
||||
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error {
|
||||
// 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
|
||||
// blocked in specific namespaces which they were not previously allowed in.
|
||||
|
|
|
@ -3,7 +3,6 @@ package cli
|
|||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
@ -19,7 +18,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
|
@ -37,12 +35,10 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
assert.Error(t, err)
|
||||
|
||||
})
|
||||
|
||||
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),
|
||||
}}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, test.isSystem)
|
||||
|
@ -81,7 +76,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
|
@ -102,7 +96,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, false)
|
||||
|
@ -125,7 +118,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
systemNamespaceLabel: "true",
|
||||
}}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, false)
|
||||
|
@ -159,7 +151,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(namespace, config),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
|
@ -178,6 +169,5 @@ func Test_ToggleSystemState(t *testing.T) {
|
|||
actualPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
assert.NoError(t, err, "failed to fetch policies")
|
||||
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.
|
||||
if hostURL == "localhost" {
|
||||
hostURL = hostURL + service.httpsBindAddr
|
||||
hostURL += service.httpsBindAddr
|
||||
baseURL = "/"
|
||||
}
|
||||
|
||||
|
|
|
@ -121,12 +121,13 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
|||
|
||||
client := &http.Client{}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -41,10 +41,12 @@ func DetermineContainerPlatform() (ContainerPlatform, error) {
|
|||
if podmanModeEnvVar == "1" {
|
||||
return PlatformPodman, nil
|
||||
}
|
||||
|
||||
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
|
||||
if serviceHostKubernetesEnvVar != "" {
|
||||
return PlatformKubernetes, nil
|
||||
}
|
||||
|
||||
nomadJobName := os.Getenv(NomadJobName)
|
||||
if nomadJobName != "" {
|
||||
return PlatformNomad, nil
|
||||
|
|
|
@ -225,7 +225,7 @@ type (
|
|||
// It contains some information of Docker's ContainerJSON struct
|
||||
DockerContainerSnapshot struct {
|
||||
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
|
||||
|
@ -388,8 +388,7 @@ type (
|
|||
LastCheckInDate int64
|
||||
// QueryDate of each query with the endpoints list
|
||||
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
|
||||
UserTrusted bool
|
||||
|
||||
|
@ -402,6 +401,8 @@ type (
|
|||
Version string `example:"1.0.0"`
|
||||
}
|
||||
|
||||
EnableGPUManagement bool `json:"EnableGPUManagement"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
|
@ -415,6 +416,9 @@ type (
|
|||
|
||||
// Deprecated in DBVersion == 22
|
||||
Tags []string `json:"Tags"`
|
||||
|
||||
// Deprecated v2.18
|
||||
IsEdgeDevice bool
|
||||
}
|
||||
|
||||
EnvironmentEdgeSettings struct {
|
||||
|
@ -502,6 +506,7 @@ type (
|
|||
// EndpointPostInitMigrations
|
||||
EndpointPostInitMigrations struct {
|
||||
MigrateIngresses bool `json:"MigrateIngresses"`
|
||||
MigrateGPUs bool `json:"MigrateGPUs"`
|
||||
}
|
||||
|
||||
// Extension represents a deprecated Portainer extension
|
||||
|
@ -585,9 +590,12 @@ type (
|
|||
Flags KubernetesFlags `json:"Flags"`
|
||||
}
|
||||
|
||||
// KubernetesFlags are used to detect if we need to run initial cluster
|
||||
// detection again.
|
||||
KubernetesFlags struct {
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerIngressClassDetected bool `json:"IsServerIngressClassDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
}
|
||||
|
||||
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
||||
|
@ -1283,8 +1291,6 @@ type (
|
|||
UserThemeSettings struct {
|
||||
// Color represents the color theme of the UI
|
||||
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
|
||||
|
@ -1507,7 +1513,7 @@ type (
|
|||
|
||||
const (
|
||||
// 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 = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
@ -1554,7 +1560,9 @@ const (
|
|||
)
|
||||
|
||||
// List of supported features
|
||||
var SupportedFeatureFlags = []featureflags.Feature{}
|
||||
var SupportedFeatureFlags = []featureflags.Feature{
|
||||
"fdo",
|
||||
}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
|
|
|
@ -24,6 +24,7 @@ func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
|
|||
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
|
||||
}
|
||||
|
||||
return filePaths
|
||||
}
|
||||
|
||||
|
|
|
@ -120,8 +120,6 @@
|
|||
--bg-navtabs-hover-color: var(--grey-16);
|
||||
--bg-nav-tab-active-color: var(--ui-gray-4);
|
||||
--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-log-viewer-color: var(--white-color);
|
||||
--bg-log-line-selected-color: var(--grey-18);
|
||||
|
@ -136,7 +134,6 @@
|
|||
--bg-item-highlighted-color: var(--grey-21);
|
||||
--bg-item-highlighted-null-color: var(--grey-14);
|
||||
--bg-panel-body-color: var(--white-color);
|
||||
--bg-codemirror-selected-color: var(--grey-22);
|
||||
--bg-tooltip-color: var(--ui-gray-11);
|
||||
--bg-input-sm-color: var(--white-color);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
|
@ -182,11 +179,7 @@
|
|||
--text-navtabs-color: var(--grey-7);
|
||||
--text-navtabs-hover-color: var(--grey-6);
|
||||
--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-log-viewer-color: var(--black-color);
|
||||
--text-json-tree-color: var(--blue-3);
|
||||
|
@ -224,7 +217,6 @@
|
|||
--border-md-checkbox-color: var(--grey-19);
|
||||
--border-modal-header-color: var(--grey-45);
|
||||
--border-navtabs-color: var(--ui-white);
|
||||
--border-codemirror-cursor-color: var(--black-color);
|
||||
--border-pre-color: var(--grey-43);
|
||||
--border-pagination-span-color: var(--ui-white);
|
||||
--border-pagination-hover-color: var(--ui-white);
|
||||
|
@ -281,9 +273,6 @@
|
|||
--bg-card-color: var(--grey-1);
|
||||
--bg-checkbox-border-color: var(--grey-8);
|
||||
--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-main-color: var(--grey-2);
|
||||
--bg-sidebar-color: var(--grey-1);
|
||||
|
@ -361,11 +350,7 @@
|
|||
--text-navtabs-color: var(--grey-8);
|
||||
--text-navtabs-hover-color: var(--grey-9);
|
||||
--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-log-viewer-color: var(--white-color);
|
||||
--text-json-tree-color: var(--grey-40);
|
||||
|
@ -403,7 +388,6 @@
|
|||
--border-md-checkbox-color: var(--grey-41);
|
||||
--border-modal-header-color: var(--grey-1);
|
||||
--border-navtabs-color: var(--grey-38);
|
||||
--border-codemirror-cursor-color: var(--white-color);
|
||||
--border-pre-color: var(--grey-3);
|
||||
--border-blocklist: var(--ui-gray-9);
|
||||
--border-blocklist-item-selected-color: var(--grey-38);
|
||||
|
@ -468,15 +452,12 @@
|
|||
--bg-switch-box-color: var(--grey-53);
|
||||
--bg-panel-body-color: var(--black-color);
|
||||
--bg-dropdown-menu-color: var(--ui-gray-warm-8);
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-motd-body-color: var(--black-color);
|
||||
--bg-blocklist-hover-color: var(--black-color);
|
||||
--bg-blocklist-item-selected-color: var(--black-color);
|
||||
--bg-input-group-addon-color: var(--grey-3);
|
||||
--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-line-selected-color: var(--grey-3);
|
||||
--bg-modal-content-color: var(--black-color);
|
||||
|
@ -536,7 +517,6 @@
|
|||
--text-tooltip-color: var(--white-color);
|
||||
--text-blocklist-item-selected-color: var(--blue-9);
|
||||
--text-input-group-addon-color: var(--white-color);
|
||||
--text-codemirror-color: var(--white-color);
|
||||
--text-dropdown-menu-color: var(--white-color);
|
||||
--text-log-viewer-color: var(--white-color);
|
||||
--text-summary-color: var(--white-color);
|
||||
|
@ -582,7 +562,6 @@
|
|||
--border-pre-next-month: var(--white-color);
|
||||
--border-daterangepicker-after: var(--black-color);
|
||||
--border-pre-color: var(--grey-3);
|
||||
--border-codemirror-cursor-color: var(--white-color);
|
||||
--border-modal: 1px solid var(--white-color);
|
||||
--border-sortbutton: var(--black-color);
|
||||
--border-bootbox: var(--black-color);
|
||||
|
@ -596,9 +575,7 @@
|
|||
|
||||
--text-input-textarea: var(--black-color);
|
||||
--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);
|
||||
|
||||
--user-menu-icon-color: var(--white-color);
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
border: 1px solid var(--border-input-group-addon-color);
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
z-index: unset;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--ui-error-9);
|
||||
}
|
||||
|
@ -150,50 +154,6 @@ code {
|
|||
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 {
|
||||
background: var(--bg-dropdown-menu-color);
|
||||
border-radius: 8px;
|
||||
|
@ -358,11 +318,6 @@ input:-webkit-autofill {
|
|||
}
|
||||
|
||||
/* Overide Vendor CSS */
|
||||
|
||||
.btn-link:hover {
|
||||
color: var(--text-link-hover-color) !important;
|
||||
}
|
||||
|
||||
.multiSelect.inlineBlock button {
|
||||
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 { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||
|
||||
import { reactModule } from './react';
|
||||
|
@ -16,14 +18,17 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
|||
abstract: true,
|
||||
onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService) {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await checkEndpointStatus(endpoint);
|
||||
|
||||
if (endpoint.Type !== 4) {
|
||||
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
|
||||
await updateEndpointStatus(endpoint, status);
|
||||
}
|
||||
endpoint.Status = status;
|
||||
|
@ -34,16 +39,22 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
|||
|
||||
await StateManager.updateEndpointState(endpoint);
|
||||
} catch (e) {
|
||||
Notifications.error('Failed loading environment', e);
|
||||
$state.go('portainer.home', {}, { reload: true });
|
||||
let params = {};
|
||||
|
||||
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) {
|
||||
try {
|
||||
await SystemService.ping(endpoint.Id);
|
||||
return 1;
|
||||
return EnvironmentStatus.Up;
|
||||
} catch (e) {
|
||||
return 2;
|
||||
return EnvironmentStatus.Down;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!-- use registry -->
|
||||
<div class="row">
|
||||
<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">
|
||||
<select
|
||||
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"
|
||||
target="_blank"
|
||||
>
|
||||
<pr-icon icon="'svg-docker'" size="'lg'"></pr-icon> Search
|
||||
<pr-icon icon="'svg-docker'" size="'md'"></pr-icon> Search
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
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 { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
||||
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
|
||||
.module('portainer.docker.react.components', [])
|
||||
|
@ -37,5 +41,28 @@ export const componentsModule = angular
|
|||
)
|
||||
.component(
|
||||
'gpu',
|
||||
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
||||
).name;
|
||||
r2a(Gpu, [
|
||||
'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',
|
||||
'$transition$',
|
||||
'$filter',
|
||||
'$analytics',
|
||||
'Container',
|
||||
'ContainerHelper',
|
||||
'Image',
|
||||
|
@ -35,6 +36,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
'FormValidator',
|
||||
'RegistryService',
|
||||
'SystemService',
|
||||
'SettingsService',
|
||||
'PluginService',
|
||||
'HttpRequestHelper',
|
||||
'endpoint',
|
||||
|
@ -46,6 +48,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
$timeout,
|
||||
$transition$,
|
||||
$filter,
|
||||
$analytics,
|
||||
Container,
|
||||
ContainerHelper,
|
||||
Image,
|
||||
|
@ -60,6 +63,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
FormValidator,
|
||||
RegistryService,
|
||||
SystemService,
|
||||
SettingsService,
|
||||
PluginService,
|
||||
HttpRequestHelper,
|
||||
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) {
|
||||
const userId = Authentication.getUserDetails().ID;
|
||||
const resourceControl = newContainer.Portainer.ResourceControl;
|
||||
|
@ -1101,7 +1117,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
return validateForm(accessControlData, $scope.isAdmin);
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
async function onSuccess() {
|
||||
await sendAnalytics();
|
||||
Notifications.success('Success', 'Container successfully created');
|
||||
$state.go('docker.containers', {}, { reload: true });
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<form class="form-horizontal" autocomplete="off">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,8 +37,6 @@
|
|||
model="formValues.RegistryModel"
|
||||
ng-if="formValues.RegistryModel.Registry"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="formValues.alwaysPull"
|
||||
|
@ -169,7 +167,7 @@
|
|||
<div class="col-sm-12">
|
||||
<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)
|
||||
|| (fromContainer.IsPortainer && fromContainer.Name === '/' + config.name)"
|
||||
ng-click="create()"
|
||||
|
@ -701,17 +699,20 @@
|
|||
</div>
|
||||
<!-- !shm-size-input -->
|
||||
<!-- #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
|
||||
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||
values="formValues.GPU"
|
||||
on-change="(onGpuChange)"
|
||||
gpus="endpoint.Gpus"
|
||||
used-gpus="gpuUseList"
|
||||
used-all-gpus="gpuUseAll"
|
||||
>
|
||||
</gpu>
|
||||
<gpu
|
||||
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||
values="formValues.GPU"
|
||||
on-change="(onGpuChange)"
|
||||
gpus="endpoint.Gpus"
|
||||
used-gpus="gpuUseList"
|
||||
used-all-gpus="gpuUseAll"
|
||||
enable-gpu-management="endpoint.EnableGPUManagement"
|
||||
>
|
||||
</gpu>
|
||||
</div>
|
||||
|
||||
<!-- #endregion GPU -->
|
||||
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
</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">
|
||||
<dashboard-item icon="'layers'" type="'Stack'" value="stackCount"></dashboard-item>
|
||||
</a>
|
||||
|
@ -106,7 +106,12 @@
|
|||
</a>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -2,10 +2,13 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
|||
|
||||
export default class DockerFeaturesConfigurationController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, EndpointService, Notifications, StateManager) {
|
||||
constructor($async, $scope, $state, $analytics, EndpointService, SettingsService, Notifications, StateManager) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.$state = $state;
|
||||
this.$analytics = $analytics;
|
||||
this.EndpointService = EndpointService;
|
||||
this.SettingsService = SettingsService;
|
||||
this.Notifications = Notifications;
|
||||
this.StateManager = StateManager;
|
||||
|
||||
|
@ -35,6 +38,8 @@ export default class DockerFeaturesConfigurationController {
|
|||
this.save = this.save.bind(this);
|
||||
this.onChangeField = this.onChangeField.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.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
||||
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
||||
|
@ -52,6 +57,12 @@ export default class DockerFeaturesConfigurationController {
|
|||
});
|
||||
}
|
||||
|
||||
onToggleGPUManagement(checked) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.state.enableGPUManagement = checked;
|
||||
});
|
||||
}
|
||||
|
||||
onChange(values) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues = {
|
||||
|
@ -69,6 +80,12 @@ export default class DockerFeaturesConfigurationController {
|
|||
};
|
||||
}
|
||||
|
||||
onGpusChange(value) {
|
||||
return this.$async(async () => {
|
||||
this.endpoint.Gpus = value;
|
||||
});
|
||||
}
|
||||
|
||||
isContainerEditDisabled() {
|
||||
const {
|
||||
disableBindMountsForRegularUsers,
|
||||
|
@ -92,7 +109,11 @@ export default class DockerFeaturesConfigurationController {
|
|||
return this.$async(async () => {
|
||||
try {
|
||||
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,
|
||||
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
||||
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
||||
|
@ -102,33 +123,53 @@ export default class DockerFeaturesConfigurationController {
|
|||
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
||||
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
||||
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');
|
||||
} catch (e) {
|
||||
this.Notifications.error('Failure', e, 'Failed saving settings');
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
this.$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
checkAgent() {
|
||||
const applicationState = this.StateManager.getState();
|
||||
return applicationState.endpoint.mode.agentProxy;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const securitySettings = this.endpoint.SecuritySettings;
|
||||
|
||||
const isAgent = this.checkAgent();
|
||||
this.isAgent = isAgent;
|
||||
const applicationState = this.StateManager.getState();
|
||||
this.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
this.isDockerStandaloneEnv = applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE';
|
||||
|
||||
this.formValues = {
|
||||
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
|
||||
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
||||
enableHostManagementFeatures: this.isAgent && securitySettings.enableHostManagementFeatures,
|
||||
allowVolumeBrowserForRegularUsers: this.isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
||||
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
||||
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
||||
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
||||
|
@ -137,5 +178,11 @@ export default class DockerFeaturesConfigurationController {
|
|||
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
||||
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 -->
|
||||
<div class="col-sm-12 form-section-title"> Other </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 pb-3">
|
||||
<gpus-insights></gpus-insights>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<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"
|
||||
name="'outOfDateImageToggle'"
|
||||
label-class="'col-sm-7 col-lg-4'"
|
||||
|
@ -166,7 +185,13 @@
|
|||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
<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-show="$ctrl.state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
|
|
|
@ -167,6 +167,6 @@ function confirmImageForceRemoval() {
|
|||
title: 'Are you sure?',
|
||||
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.',
|
||||
confirmButton: buildConfirmButton('Remote the image', 'danger'),
|
||||
confirmButton: buildConfirmButton('Remove the image', 'danger'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,19 +1,5 @@
|
|||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
||||
<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-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
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">
|
||||
<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">
|
||||
|
|
|
@ -37,6 +37,7 @@ export class EdgeGroupFormController {
|
|||
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
||||
this.onChangeModel = this.onChangeModel.bind(this);
|
||||
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
$scope.$watch(
|
||||
() => this.model,
|
||||
|
@ -118,6 +119,10 @@ export class EdgeGroupFormController {
|
|||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
this.formAction(this.model);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.getTags();
|
||||
}
|
||||
|
|
|
@ -7,12 +7,19 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
|
|||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.edge.react.components', [])
|
||||
.component(
|
||||
'edgeGroupsSelector',
|
||||
r2a(EdgeGroupsSelector, ['items', 'onChange', 'value'])
|
||||
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
||||
'onChange',
|
||||
'value',
|
||||
'error',
|
||||
'horizontal',
|
||||
'isGroupVisible',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'edgeScriptForm',
|
||||
|
@ -21,6 +28,7 @@ export const componentsModule = angular
|
|||
'commands',
|
||||
'isNomadTokenVisible',
|
||||
'asyncMode',
|
||||
'showMetaFields',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
|
|
|
@ -21,7 +21,6 @@ export class CreateEdgeGroupController {
|
|||
};
|
||||
|
||||
this.createGroup = this.createGroup.bind(this);
|
||||
this.createGroupAsync = this.createGroupAsync.bind(this);
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
|
@ -31,20 +30,18 @@ export class CreateEdgeGroupController {
|
|||
this.state.loaded = true;
|
||||
}
|
||||
|
||||
createGroup() {
|
||||
return this.$async(this.createGroupAsync);
|
||||
}
|
||||
|
||||
async createGroupAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.EdgeGroupService.create(this.model);
|
||||
this.Notifications.success('Success', 'Edge group successfully created');
|
||||
this.$state.go('edge.groups');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
async createGroup(model) {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.EdgeGroupService.create(model);
|
||||
this.Notifications.success('Success', 'Edge group successfully created');
|
||||
this.$state.go('edge.groups');
|
||||
} catch (err) {
|
||||
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.updateGroupAsync = this.updateGroupAsync.bind(this);
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
|
@ -28,20 +27,18 @@ export class EditEdgeGroupController {
|
|||
this.state.loaded = true;
|
||||
}
|
||||
|
||||
updateGroup() {
|
||||
return this.$async(this.updateGroupAsync);
|
||||
}
|
||||
|
||||
async updateGroupAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.EdgeGroupService.update(this.model);
|
||||
this.Notifications.success('Success', 'Edge group successfully updated');
|
||||
this.$state.go('edge.groups');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
updateGroup(group) {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
await this.EdgeGroupService.update(group);
|
||||
this.Notifications.success('Success', 'Edge group successfully updated');
|
||||
this.$state.go('edge.groups');
|
||||
} catch (err) {
|
||||
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() {
|
||||
try {
|
||||
this.edgeGroups = await this.EdgeGroupService.groups();
|
||||
this.noGroups = this.edgeGroups.length === 0;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
||||
}
|
||||
|
|
|
@ -39,19 +39,7 @@
|
|||
</div>
|
||||
<!-- !name-input -->
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
||||
<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-groups-selector value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
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 customTemplateModule from './custom-templates';
|
||||
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) {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (endpoint.Type === 7) {
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
//edge
|
||||
try {
|
||||
await KubernetesHealthService.ping(endpoint.Id);
|
||||
endpoint.Status = 1;
|
||||
endpoint.Status = EnvironmentStatus.Up;
|
||||
} catch (e) {
|
||||
endpoint.Status = 2;
|
||||
endpoint.Status = EnvironmentStatus.Down;
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
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) {
|
||||
Notifications.error('Failed loading environment', e);
|
||||
$state.go('portainer.home', {}, { reload: true });
|
||||
let params = {};
|
||||
|
||||
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">
|
||||
<!-- toolbar header actions and settings -->
|
||||
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col gap-1">
|
||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
||||
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col !gap-0">
|
||||
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="'box'"></pr-icon>
|
||||
</div>
|
||||
Applications
|
||||
</div>
|
||||
<div class="searchBar vertical-center !mr-0 min-w-[280px]">
|
||||
<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 for an application..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sApp-searchApplicationsInput"
|
||||
/>
|
||||
</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"
|
||||
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="settings" data-cy="k8sApp-tableSettings">
|
||||
<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>
|
||||
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||
<div class="settings" data-cy="k8sApp-tableSettings">
|
||||
<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>
|
||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||
<div>
|
||||
<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>
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||
<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.
|
||||
</span>
|
||||
</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>
|
||||
<!-- data table content -->
|
||||
<div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
|
||||
|
|
|
@ -15,5 +15,10 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
|
|||
refreshCallback: '<',
|
||||
onPublishingModeClick: '<',
|
||||
isPrimary: '<',
|
||||
namespaces: '<',
|
||||
namespace: '<',
|
||||
onChangeNamespaceDropdown: '<',
|
||||
isSystemResources: '<',
|
||||
setSystemResources: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -21,6 +21,8 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
this.state = Object.assign(this.state, {
|
||||
expandAll: false,
|
||||
expandedItems: [],
|
||||
namespace: '',
|
||||
namespaces: [],
|
||||
});
|
||||
|
||||
this.filters = {
|
||||
|
@ -70,6 +72,8 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
};
|
||||
|
||||
this.onSettingsShowSystemChange = function () {
|
||||
this.updateNamespace();
|
||||
this.setSystemResources(this.settings.showSystem);
|
||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||
};
|
||||
|
||||
|
@ -135,6 +139,45 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
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.isAdmin = Authentication.isAdmin();
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
|
@ -172,7 +215,16 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
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();
|
||||
};
|
||||
},
|
||||
|
|
|
@ -1,99 +1,122 @@
|
|||
<div class="datatable">
|
||||
<!-- table title and action menu -->
|
||||
<div class="toolBar !flex-col gap-1">
|
||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
||||
<!-- title -->
|
||||
<div class="toolBar !flex-col !gap-0">
|
||||
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="'list'"></pr-icon>
|
||||
</div>
|
||||
Stacks
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<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 for a stack..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
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-removeStackButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
||||
<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>
|
||||
<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-stack"
|
||||
/>
|
||||
<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-stack"
|
||||
>
|
||||
<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>
|
||||
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
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-removeStackButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
||||
<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>
|
||||
<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-stack"
|
||||
/>
|
||||
<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-stack"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<!-- info text -->
|
||||
<div class="flex w-full flex-row">
|
||||
<span class="small text-muted vertical-center mt-1" 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">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
System resources are hidden, this can be changed in the table settings.
|
||||
</span>
|
||||
</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 class="table-responsive">
|
||||
<table class="table-hover nowrap-cells table">
|
||||
|
|
|
@ -10,5 +10,10 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDa
|
|||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
removeAction: '<',
|
||||
namespaces: '<',
|
||||
namespace: '<',
|
||||
onChangeNamespaceDropdown: '<',
|
||||
isSystemResources: '<',
|
||||
setSystemResources: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
|||
this.state = Object.assign(this.state, {
|
||||
expandedItems: [],
|
||||
expandAll: false,
|
||||
namespace: '',
|
||||
namespaces: [],
|
||||
});
|
||||
|
||||
var ctrl = this;
|
||||
|
@ -22,6 +24,8 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
|||
});
|
||||
|
||||
this.onSettingsShowSystemChange = function () {
|
||||
this.updateNamespace();
|
||||
this.setSystemResources(this.settings.showSystem);
|
||||
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.isAdmin = Authentication.isAdmin();
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
|
@ -103,11 +145,20 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
|||
this.filters.state.open = false;
|
||||
}
|
||||
|
||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||
var storedSettings = DatatableService.getDataTableSettings(this.settingsKey);
|
||||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
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();
|
||||
};
|
||||
},
|
||||
|
|
|
@ -141,7 +141,9 @@ export default class HelmTemplatesController {
|
|||
try {
|
||||
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.resourcePool = this.state.resourcePools[0];
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
placeholder="# Define or paste the content of your manifest here"
|
||||
read-only="true"
|
||||
hide-title="true"
|
||||
height="{{ $ctrl.expanded ? '800px' : '500px' }}"
|
||||
>
|
||||
</web-editor-form>
|
||||
<div class="py-5">
|
||||
|
|
|
@ -33,9 +33,6 @@ class KubernetesYamlInspectorController {
|
|||
}
|
||||
|
||||
toggleYAMLInspectorExpansion() {
|
||||
let selector = 'kubernetes-yaml-inspector code-editor > div.CodeMirror';
|
||||
let height = this.expanded ? '500px' : '80vh';
|
||||
$(selector).css({ height: height });
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
|||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable';
|
||||
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
|
||||
import { DashboardView } from '@/react/kubernetes/DashboardView';
|
||||
import { ServicesView } from '@/react/kubernetes/ServicesView';
|
||||
|
||||
export const viewsModule = angular
|
||||
|
@ -24,4 +25,8 @@ export const viewsModule = angular
|
|||
.component(
|
||||
'kubernetesIngressesCreateView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), [])
|
||||
)
|
||||
.component(
|
||||
'kubernetesDashboardView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
|
||||
).name;
|
||||
|
|
|
@ -7,9 +7,10 @@ import $allSettled from 'Portainer/services/allSettled';
|
|||
|
||||
class KubernetesNamespaceService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesNamespaces) {
|
||||
constructor($async, KubernetesNamespaces, LocalStorage) {
|
||||
this.$async = $async;
|
||||
this.KubernetesNamespaces = KubernetesNamespaces;
|
||||
this.LocalStorage = LocalStorage;
|
||||
|
||||
this.getAsync = this.getAsync.bind(this);
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
|
@ -17,6 +18,7 @@ class KubernetesNamespaceService {
|
|||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
this.getJSONAsync = this.getJSONAsync.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) {
|
||||
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 params = {};
|
||||
const data = await this.KubernetesNamespaces().create(params, payload).$promise;
|
||||
await this.refreshCacheAsync();
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create namespace', err);
|
||||
|
@ -104,6 +116,14 @@ class KubernetesNamespaceService {
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -36,8 +36,8 @@ export function KubernetesResourcePoolService(
|
|||
}
|
||||
|
||||
// getting the quota for all namespaces is costly by default, so disable getting it by default
|
||||
async function getAll({ getQuota = false }) {
|
||||
const namespaces = await KubernetesNamespaceService.get();
|
||||
async function getAll({ getQuota = false, refreshCache = false }) {
|
||||
const namespaces = await KubernetesNamespaceService.get('', refreshCache);
|
||||
const pools = await Promise.all(
|
||||
_.map(namespaces, async (namespace) => {
|
||||
const name = namespace.Name;
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
refresh-callback="ctrl.getApplications"
|
||||
on-publishing-mode-click="(ctrl.onPublishingModeClick)"
|
||||
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>
|
||||
</uib-tab>
|
||||
|
@ -30,6 +35,11 @@
|
|||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
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>
|
||||
</uib-tab>
|
||||
|
|
|
@ -9,9 +9,23 @@ import { confirmDelete } from '@@/modals/confirm';
|
|||
|
||||
class KubernetesApplicationsController {
|
||||
/* @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.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.Authentication = Authentication;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.HelmService = HelmService;
|
||||
|
@ -19,6 +33,7 @@ class KubernetesApplicationsController {
|
|||
this.Authentication = Authentication;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.StackService = StackService;
|
||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getApplications = this.getApplications.bind(this);
|
||||
|
@ -28,6 +43,8 @@ class KubernetesApplicationsController {
|
|||
this.removeStacksAction = this.removeStacksAction.bind(this);
|
||||
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
||||
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
||||
this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
|
||||
this.setSystemResources = this.setSystemResources.bind(this);
|
||||
}
|
||||
|
||||
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() {
|
||||
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 { helmApplications, nonHelmApplications } = KubernetesApplicationHelper.getNestedApplications(configuredApplications);
|
||||
|
||||
this.state.applications = [...helmApplications, ...nonHelmApplications];
|
||||
this.state.stacks = KubernetesStackHelper.stacksFromApplications(applications);
|
||||
this.state.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications);
|
||||
|
||||
this.$scope.$apply();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
}
|
||||
}
|
||||
|
||||
setSystemResources(flag) {
|
||||
this.state.isSystemResources = flag;
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
@ -153,7 +186,27 @@ class KubernetesApplicationsController {
|
|||
applications: [],
|
||||
stacks: [],
|
||||
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();
|
||||
|
||||
this.state.viewReady = true;
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
</div>
|
||||
<!-- #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>
|
||||
</div>
|
||||
|
||||
|
@ -336,7 +336,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-4">
|
||||
<div class="col-sm-12 mt-2">
|
||||
<span
|
||||
ng-if="ctrl.formValues.Containers.length <= 1"
|
||||
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
||||
|
@ -503,7 +503,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1208,22 +1208,31 @@ class KubernetesCreateApplicationController {
|
|||
]);
|
||||
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.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
|
||||
const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota;
|
||||
if (!this.formValues.ResourcePool) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.state.nodes.memory and this.state.nodes.cpu are used to calculate the slider limits, so set them before calling updateSliders()
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||
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.nodeNumber = nodes.length;
|
||||
|
||||
|
@ -1281,9 +1290,6 @@ class KubernetesCreateApplicationController {
|
|||
this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;
|
||||
|
||||
this.oldFormValues = angular.copy(this.formValues);
|
||||
|
||||
this.updateNamespaceLimits(namespaceWithQuota);
|
||||
this.updateSliders(namespaceWithQuota);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||
<!-- resource-pool -->
|
||||
<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">
|
||||
<select
|
||||
class="form-control"
|
||||
|
|
|
@ -196,7 +196,10 @@ class KubernetesCreateConfigurationController {
|
|||
|
||||
try {
|
||||
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];
|
||||
await this.getConfigurations();
|
||||
|
|
|
@ -165,7 +165,10 @@ class KubernetesConfigureController {
|
|||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||
const resourcePools = _.filter(
|
||||
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) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue