diff --git a/api/archive/targz_test.go b/api/archive/targz_test.go index df17b8f4f..f3694f16f 100644 --- a/api/archive/targz_test.go +++ b/api/archive/targz_test.go @@ -2,7 +2,6 @@ package archive import ( "fmt" - "io/ioutil" "os" "os/exec" "path" @@ -28,10 +27,10 @@ func listFiles(dir string) []string { func Test_shouldCreateArhive(t *testing.T) { tmpdir := t.TempDir() content := []byte("content") - ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600) + os.WriteFile(path.Join(tmpdir, "outer"), content, 0600) os.MkdirAll(path.Join(tmpdir, "dir"), 0700) - ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600) - ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600) + os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600) + os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600) gzPath, err := TarGzDir(tmpdir) assert.Nil(t, err) @@ -48,7 +47,7 @@ func Test_shouldCreateArhive(t *testing.T) { wasExtracted := func(p string) { fullpath := path.Join(extractionDir, p) assert.Contains(t, extractedFiles, fullpath) - copyContent, _ := ioutil.ReadFile(fullpath) + copyContent, _ := os.ReadFile(fullpath) assert.Equal(t, content, copyContent) } @@ -60,10 +59,10 @@ func Test_shouldCreateArhive(t *testing.T) { func Test_shouldCreateArhiveXXXXX(t *testing.T) { tmpdir := t.TempDir() content := []byte("content") - ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600) + os.WriteFile(path.Join(tmpdir, "outer"), content, 0600) os.MkdirAll(path.Join(tmpdir, "dir"), 0700) - ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600) - ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600) + os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600) + os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600) gzPath, err := TarGzDir(tmpdir) assert.Nil(t, err) @@ -80,7 +79,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) { wasExtracted := func(p string) { fullpath := path.Join(extractionDir, p) assert.Contains(t, extractedFiles, fullpath) - copyContent, _ := ioutil.ReadFile(fullpath) + copyContent, _ := os.ReadFile(fullpath) assert.Equal(t, content, copyContent) } diff --git a/api/archive/zip.go b/api/archive/zip.go index 2aaee5a4f..50328ef09 100644 --- a/api/archive/zip.go +++ b/api/archive/zip.go @@ -4,12 +4,12 @@ import ( "archive/zip" "bytes" "fmt" - "github.com/pkg/errors" "io" - "io/ioutil" "os" "path/filepath" "strings" + + "github.com/pkg/errors" ) // UnzipArchive will unzip an archive from bytes into the dest destination folder on disk @@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error { } defer f.Close() - data, err := ioutil.ReadAll(f) + data, err := io.ReadAll(f) if err != nil { return err } diff --git a/api/archive/zip_test.go b/api/archive/zip_test.go index 268109358..056bc1c0d 100644 --- a/api/archive/zip_test.go +++ b/api/archive/zip_test.go @@ -1,9 +1,10 @@ package archive import ( - "github.com/stretchr/testify/assert" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" ) func TestUnzipFile(t *testing.T) { diff --git a/api/cli/cli.go b/api/cli/cli.go index 83ced70ea..69dd61b75 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -62,6 +62,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(), SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/.").Default(defaultSecretKeyName).String(), LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"), + LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"), } kingpin.Parse() diff --git a/api/cmd/portainer/import.go b/api/cmd/portainer/import.go deleted file mode 100644 index 7ce53f4a8..000000000 --- a/api/cmd/portainer/import.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/datastore" - - "github.com/rs/zerolog/log" -) - -func importFromJson(fileService portainer.FileService, store *datastore.Store) { - // EXPERIMENTAL - if used with an incomplete json file, it will fail, as we don't have a way to default the model values - importFile := "/data/import.json" - if exists, _ := fileService.FileExists(importFile); exists { - if err := store.Import(importFile); err != nil { - log.Error().Str("filename", importFile).Err(err).Msg("import failed") - // TODO: should really rollback on failure, but then we have nothing. - } else { - log.Info().Str("filename", importFile).Msg("successfully imported the file to a new portainer database") - } - - // TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want - // I also suspect that everything from "Init to Init" is potentially a migration - err := store.Init() - if err != nil { - log.Fatal().Err(err).Msg("failed initializing data store") - } - } -} diff --git a/api/cmd/portainer/log.go b/api/cmd/portainer/log.go index 1623b4fd7..ef95b26b0 100644 --- a/api/cmd/portainer/log.go +++ b/api/cmd/portainer/log.go @@ -1,7 +1,9 @@ package main import ( + "fmt" stdlog "log" + "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -31,3 +33,23 @@ func setLoggingLevel(level string) { zerolog.SetGlobalLevel(zerolog.DebugLevel) } } + +func setLoggingMode(mode string) { + switch mode { + case "PRETTY": + log.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stderr, + NoColor: true, + TimeFormat: "2006/01/02 03:04PM", + FormatMessage: formatMessage}) + case "JSON": + log.Logger = log.Output(os.Stderr) + } +} + +func formatMessage(i interface{}) string { + if i == nil { + return "" + } + return fmt.Sprintf("%s |", i) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index ed503ad65..031482b8d 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -239,8 +239,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService, return docker.NewClientFactory(signatureService, reverseTunnelService) } -func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory { - return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore) +func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) { + return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout) } func initSnapshotService( @@ -316,12 +316,7 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer. sslSettings.HTTPEnabled = true } - err = dataStore.SSLSettings().UpdateSettings(sslSettings) - if err != nil { - return err - } - - return nil + return dataStore.SSLSettings().UpdateSettings(sslSettings) } // enableFeaturesFromFlags turns on or off feature flags @@ -617,7 +612,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { reverseTunnelService := chisel.NewService(dataStore, shutdownCtx) dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) - kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore) + kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout) snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx) if err != nil { @@ -763,10 +758,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { func main() { configureLogger() + setLoggingMode("PRETTY") flags := initCLI() setLoggingLevel(*flags.LogLevel) + setLoggingMode(*flags.LogMode) for { server := buildServer(flags) diff --git a/api/connection.go b/api/connection.go index 643767058..6c9e4f440 100644 --- a/api/connection.go +++ b/api/connection.go @@ -24,6 +24,7 @@ type Connection interface { SetServiceName(bucketName string) error GetObject(bucketName string, key []byte, object interface{}) error UpdateObject(bucketName string, key []byte, object interface{}) error + UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error DeleteObject(bucketName string, key []byte) error DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error GetNextIdentifier(bucketName string) int diff --git a/api/crypto/aes.go b/api/crypto/aes.go index 5a6f58fe7..6dd273310 100644 --- a/api/crypto/aes.go +++ b/api/crypto/aes.go @@ -13,7 +13,7 @@ import ( // Person with better knowledge is welcomed to improve it. // sourced from https://golang.org/src/crypto/cipher/example_test.go -var emptySalt []byte = make([]byte, 0, 0) +var emptySalt []byte = make([]byte, 0) // AesEncrypt reads from input, encrypts with AES-256 and writes to the output. // passphrase is used to generate an encryption key. diff --git a/api/crypto/aes_test.go b/api/crypto/aes_test.go index b53d0e87d..09dea9c6d 100644 --- a/api/crypto/aes_test.go +++ b/api/crypto/aes_test.go @@ -2,7 +2,6 @@ package crypto import ( "io" - "io/ioutil" "os" "path/filepath" "testing" @@ -20,7 +19,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) { ) content := []byte("content") - ioutil.WriteFile(originFilePath, content, 0600) + os.WriteFile(originFilePath, content, 0600) originFile, _ := os.Open(originFilePath) defer originFile.Close() @@ -30,7 +29,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) { err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase")) assert.Nil(t, err, "Failed to encrypt a file") - encryptedContent, err := ioutil.ReadFile(encryptedFilePath) + encryptedContent, err := os.ReadFile(encryptedFilePath) assert.Nil(t, err, "Couldn't read encrypted file") assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted") @@ -45,7 +44,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) { io.Copy(decryptedFileWriter, decryptedReader) - decryptedContent, _ := ioutil.ReadFile(decryptedFilePath) + decryptedContent, _ := os.ReadFile(decryptedFilePath) assert.Equal(t, content, decryptedContent, "Original and decrypted content should match") } @@ -59,7 +58,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) { ) content := []byte("content") - ioutil.WriteFile(originFilePath, content, 0600) + os.WriteFile(originFilePath, content, 0600) originFile, _ := os.Open(originFilePath) defer originFile.Close() @@ -69,7 +68,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) { err := AesEncrypt(originFile, encryptedFileWriter, []byte("")) assert.Nil(t, err, "Failed to encrypt a file") - encryptedContent, err := ioutil.ReadFile(encryptedFilePath) + encryptedContent, err := os.ReadFile(encryptedFilePath) assert.Nil(t, err, "Couldn't read encrypted file") assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted") @@ -84,7 +83,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) { io.Copy(decryptedFileWriter, decryptedReader) - decryptedContent, _ := ioutil.ReadFile(decryptedFilePath) + decryptedContent, _ := os.ReadFile(decryptedFilePath) assert.Equal(t, content, decryptedContent, "Original and decrypted content should match") } @@ -98,7 +97,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) ) content := []byte("content") - ioutil.WriteFile(originFilePath, content, 0600) + os.WriteFile(originFilePath, content, 0600) originFile, _ := os.Open(originFilePath) defer originFile.Close() @@ -108,7 +107,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase")) assert.Nil(t, err, "Failed to encrypt a file") - encryptedContent, err := ioutil.ReadFile(encryptedFilePath) + encryptedContent, err := os.ReadFile(encryptedFilePath) assert.Nil(t, err, "Couldn't read encrypted file") assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted") @@ -123,6 +122,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) io.Copy(decryptedFileWriter, decryptedReader) - decryptedContent, _ := ioutil.ReadFile(decryptedFilePath) + decryptedContent, _ := os.ReadFile(decryptedFilePath) assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match") } diff --git a/api/crypto/tls.go b/api/crypto/tls.go index e46998898..e3418f66e 100644 --- a/api/crypto/tls.go +++ b/api/crypto/tls.go @@ -3,7 +3,7 @@ package crypto import ( "crypto/tls" "crypto/x509" - "io/ioutil" + "os" ) // CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings @@ -63,7 +63,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe } if !skipServerVerification && caCertPath != "" { - caCert, err := ioutil.ReadFile(caCertPath) + caCert, err := os.ReadFile(caCertPath) if err != nil { return nil, err } diff --git a/api/database/boltdb/db.go b/api/database/boltdb/db.go index 203f9255c..c7fa80bd5 100644 --- a/api/database/boltdb/db.go +++ b/api/database/boltdb/db.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path" "time" @@ -167,7 +166,7 @@ func (connection *DbConnection) ExportRaw(filename string) error { if err != nil { return err } - return ioutil.WriteFile(filename, b, 0600) + return os.WriteFile(filename, b, 0600) } // ConvertToKey returns an 8-byte big endian representation of v. @@ -179,7 +178,7 @@ func (connection *DbConnection) ConvertToKey(v int) []byte { return b } -// CreateBucket is a generic function used to create a bucket inside a database database. +// CreateBucket is a generic function used to create a bucket inside a database. func (connection *DbConnection) SetServiceName(bucketName string) error { return connection.Batch(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(bucketName)) @@ -187,7 +186,7 @@ func (connection *DbConnection) SetServiceName(bucketName string) error { }) } -// GetObject is a generic function used to retrieve an unmarshalled object from a database database. +// GetObject is a generic function used to retrieve an unmarshalled object from a database. func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error { var data []byte @@ -219,7 +218,7 @@ func (connection *DbConnection) getEncryptionKey() []byte { return connection.EncryptionKey } -// UpdateObject is a generic function used to update an object inside a database database. +// UpdateObject is a generic function used to update an object inside a database. func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error { data, err := connection.MarshalObject(object) if err != nil { @@ -232,7 +231,33 @@ func (connection *DbConnection) UpdateObject(bucketName string, key []byte, obje }) } -// DeleteObject is a generic function used to delete an object inside a database database. +// UpdateObjectFunc is a generic function used to update an object safely without race conditions. +func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error { + return connection.Batch(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(bucketName)) + + data := bucket.Get(key) + if data == nil { + return dserrors.ErrObjectNotFound + } + + err := connection.UnmarshalObjectWithJsoniter(data, object) + if err != nil { + return err + } + + updateFn() + + data, err = connection.MarshalObject(object) + if err != nil { + return err + } + + return bucket.Put(key, data) + }) +} + +// DeleteObject is a generic function used to delete an object inside a database. func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error { return connection.Batch(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(bucketName)) diff --git a/api/database/models/ingress.go b/api/database/models/ingress.go index 393ed0363..8e4f59886 100644 --- a/api/database/models/ingress.go +++ b/api/database/models/ingress.go @@ -59,9 +59,6 @@ func (r K8sIngressInfo) Validate(request *http.Request) error { if r.Namespace == "" { return errors.New("missing ingress Namespace from the request payload") } - if r.ClassName == "" { - return errors.New("missing ingress ClassName from the request payload") - } return nil } diff --git a/api/dataservices/interface.go b/api/dataservices/interface.go index 619cc6f4d..e936f5de1 100644 --- a/api/dataservices/interface.go +++ b/api/dataservices/interface.go @@ -251,6 +251,7 @@ type ( Tag(ID portainer.TagID) (*portainer.Tag, error) Create(tag *portainer.Tag) error UpdateTag(ID portainer.TagID, tag *portainer.Tag) error + UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error DeleteTag(ID portainer.TagID) error BucketName() string } diff --git a/api/dataservices/tag/tag.go b/api/dataservices/tag/tag.go index 42c289598..7641afc37 100644 --- a/api/dataservices/tag/tag.go +++ b/api/dataservices/tag/tag.go @@ -80,12 +80,24 @@ func (service *Service) Create(tag *portainer.Tag) error { ) } -// UpdateTag updates a tag. +// Deprecated: Use UpdateTagFunc instead. func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error { identifier := service.connection.ConvertToKey(int(ID)) return service.connection.UpdateObject(BucketName, identifier, tag) } +// UpdateTagFunc updates a tag inside a transaction avoiding data races. +func (service *Service) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error { + id := service.connection.ConvertToKey(int(ID)) + tag := &portainer.Tag{} + + service.connection.UpdateObjectFunc(BucketName, id, tag, func() { + updateFunc(tag) + }) + + return nil +} + // DeleteTag deletes a tag. func (service *Service) DeleteTag(ID portainer.TagID) error { identifier := service.connection.ConvertToKey(int(ID)) diff --git a/api/datastore/migrate_data_test.go b/api/datastore/migrate_data_test.go index a1b52e860..41af3c43a 100644 --- a/api/datastore/migrate_data_test.go +++ b/api/datastore/migrate_data_test.go @@ -275,7 +275,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error { // Compare the result we got with the one we wanted. if diff := cmp.Diff(wantJSON, gotJSON); diff != "" { - gotPath := filepath.Join(t.TempDir(), "portainer-migrator-test-fail.json") + gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json") os.WriteFile( gotPath, gotJSON, diff --git a/api/datastore/migrator/migrate_dbversion34_test.go b/api/datastore/migrator/migrate_dbversion34_test.go deleted file mode 100644 index 5938a4caa..000000000 --- a/api/datastore/migrator/migrate_dbversion34_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package migrator - -const ( - db35TestFile = "portainer-mig-35.db" - username = "portainer" - password = "password" -) - -// TODO: this is exactly the kind of reaching into the internals of the store we should not do -// func setupDB35Test(t *testing.T) *Migrator { -// is := assert.New(t) -// dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second}) -// is.NoError(err, "failed to init testing DB connection") - -// // Create an old style dockerhub authenticated account -// dockerhubService, err := dockerhub.NewService(&database.DbConnection{DB: dbConn}) -// is.NoError(err, "failed to init testing registry service") -// err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password}) -// is.NoError(err, "failed to create dockerhub account") - -// registryService, err := registry.NewService(&database.DbConnection{DB: dbConn}) -// is.NoError(err, "failed to init testing registry service") - -// endpointService, err := endpoint.NewService(&database.DbConnection{DB: dbConn}) -// is.NoError(err, "failed to init endpoint service") - -// m := &Migrator{ -// db: dbConn, -// dockerhubService: dockerhubService, -// registryService: registryService, -// endpointService: endpointService, -// } - -// return m -// } - -// // TestUpdateDockerhubToDB32 tests a normal upgrade -// func TestUpdateDockerhubToDB32(t *testing.T) { -// is := assert.New(t) -// m := setupDB35Test(t) -// defer m.db.Close() -// defer os.Remove(db35TestFile) - -// if err := m.updateDockerhubToDB32(); err != nil { -// t.Errorf("failed to update settings: %v", err) -// } - -// // Verify we have a single registry were created -// registries, err := m.registryService.Registries() -// is.NoError(err, "failed to read registries from the RegistryService") -// is.Equal(len(registries), 1, "only one migrated registry expected") -// } - -// // TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration -// // created a large number of duplicate "dockerhub migrated" registry entries. -// func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) { -// is := assert.New(t) -// m := setupDB35Test(t) -// defer m.db.Close() -// defer os.Remove(db35TestFile) - -// // Create lots of duplicate entries... -// registry := &portainer.Registry{ -// Type: portainer.DockerHubRegistry, -// Name: "Dockerhub (authenticated - migrated)", -// URL: "docker.io", -// Authentication: true, -// Username: "portainer", -// Password: "password", -// RegistryAccesses: portainer.RegistryAccesses{}, -// } - -// for i := 1; i < 150; i++ { -// err := m.registryService.CreateRegistry(registry) -// assert.NoError(t, err, "create registry failed") -// } - -// // Verify they were created -// registries, err := m.registryService.Registries() -// is.NoError(err, "failed to read registries from the RegistryService") -// is.Condition(func() bool { -// return len(registries) > 1 -// }, "expected multiple duplicate registry entries") - -// // Now run the migrator -// if err := m.updateDockerhubToDB32(); err != nil { -// t.Errorf("failed to update settings: %v", err) -// } - -// // Verify we have a single registry were created -// registries, err = m.registryService.Registries() -// is.NoError(err, "failed to read registries from the RegistryService") -// is.Equal(len(registries), 1, "only one migrated registry expected") -// } diff --git a/api/datastore/migrator/migrate_dbversion70.go b/api/datastore/migrator/migrate_dbversion70.go index ff121f598..5aadff56c 100644 --- a/api/datastore/migrator/migrate_dbversion70.go +++ b/api/datastore/migrator/migrate_dbversion70.go @@ -8,7 +8,7 @@ import ( func (m *Migrator) migrateDBVersionToDB70() error { log.Info().Msg("- add IngressAvailabilityPerNamespace field") - if err := m.addIngressAvailabilityPerNamespaceFieldDB70(); err != nil { + if err := m.updateIngressFieldsForEnvDB70(); err != nil { return err } @@ -51,7 +51,7 @@ func (m *Migrator) migrateDBVersionToDB70() error { return nil } -func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error { +func (m *Migrator) updateIngressFieldsForEnvDB70() error { endpoints, err := m.endpointService.Endpoints() if err != nil { return err @@ -59,6 +59,7 @@ func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error { for _, endpoint := range endpoints { endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true + endpoint.Kubernetes.Configuration.AllowNoneIngressClass = false err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { diff --git a/api/datastore/services.go b/api/datastore/services.go index c4bd90003..eb05b3329 100644 --- a/api/datastore/services.go +++ b/api/datastore/services.go @@ -3,7 +3,7 @@ package datastore import ( "encoding/json" "fmt" - "io/ioutil" + "os" "strconv" portainer "github.com/portainer/portainer/api" @@ -607,13 +607,13 @@ func (store *Store) Export(filename string) (err error) { if err != nil { return err } - return ioutil.WriteFile(filename, b, 0600) + return os.WriteFile(filename, b, 0600) } func (store *Store) Import(filename string) (err error) { backup := storeExport{} - s, err := ioutil.ReadFile(filename) + s, err := os.ReadFile(filename) if err != nil { return err } diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 99f22dea1..d21359eb6 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -52,6 +52,7 @@ "IsEdgeDevice": false, "Kubernetes": { "Configuration": { + "AllowNoneIngressClass": false, "EnableResourceOverCommit": false, "IngressAvailabilityPerNamespace": true, "IngressClasses": null, diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index 2e9119259..a6136998a 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -159,7 +159,7 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) gpuUseSet := make(map[string]struct{}) gpuUseAll := false for _, container := range containers { - if container.State == "exited" { + if container.State == "exited" || container.State == "stopped" { stoppedContainers++ } else if container.State == "running" { runningContainers++ diff --git a/api/exec/compose_stack_test.go b/api/exec/compose_stack_test.go index 733eb7fb2..b1904438a 100644 --- a/api/exec/compose_stack_test.go +++ b/api/exec/compose_stack_test.go @@ -1,7 +1,7 @@ package exec import ( - "io/ioutil" + "io" "os" "path" "testing" @@ -55,7 +55,7 @@ func Test_createEnvFile(t *testing.T) { assert.Equal(t, "stack.env", result) f, _ := os.Open(path.Join(dir, "stack.env")) - content, _ := ioutil.ReadAll(f) + content, _ := io.ReadAll(f) assert.Equal(t, tt.expected, string(content)) } else { @@ -80,7 +80,7 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) { assert.NoError(t, err) assert.FileExists(t, path.Join(dir, "stack.env")) f, _ := os.Open(path.Join(dir, "stack.env")) - content, _ := ioutil.ReadAll(f) + content, _ := io.ReadAll(f) assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content) } diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 78686333c..114ac3fd4 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -203,12 +203,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string headersObject["X-PortainerAgent-Signature"] = signature headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey() - err = manager.fileService.WriteJSONToFile(configFilePath, config) - if err != nil { - return err - } - - return nil + return manager.fileService.WriteJSONToFile(configFilePath, config) } func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) { diff --git a/api/filesystem/copy_test.go b/api/filesystem/copy_test.go index a451fdb8f..a91754975 100644 --- a/api/filesystem/copy_test.go +++ b/api/filesystem/copy_test.go @@ -1,7 +1,6 @@ package filesystem import ( - "io/ioutil" "os" "path" "path/filepath" @@ -19,12 +18,12 @@ func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) { func Test_copyFile_shouldMakeAbackup(t *testing.T) { tmpdir := t.TempDir() content := []byte("content") - ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600) + os.WriteFile(path.Join(tmpdir, "origin"), content, 0600) err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy")) assert.NoError(t, err) - copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy")) + copyContent, _ := os.ReadFile(path.Join(tmpdir, "copy")) assert.Equal(t, content, copyContent) } @@ -59,13 +58,13 @@ func Test_CopyPath_shouldSkipWhenNotExist(t *testing.T) { func Test_CopyPath_shouldCopyFile(t *testing.T) { tmpdir := t.TempDir() content := []byte("content") - ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600) + os.WriteFile(path.Join(tmpdir, "file"), content, 0600) os.MkdirAll(path.Join(tmpdir, "backup"), 0700) err := CopyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup")) assert.NoError(t, err) - copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file")) + copyContent, err := os.ReadFile(path.Join(tmpdir, "backup", "file")) assert.NoError(t, err) assert.Equal(t, content, copyContent) } diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index f0202feec..981f1ea97 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -163,7 +163,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi } if !exists { - return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath)) + return fmt.Errorf("File (%s) doesn't exist", fromFilePath) } finput, err := os.Open(fromFilePath) diff --git a/api/filesystem/write_test.go b/api/filesystem/write_test.go index 89223a20e..17606ace0 100644 --- a/api/filesystem/write_test.go +++ b/api/filesystem/write_test.go @@ -1,7 +1,7 @@ package filesystem import ( - "io/ioutil" + "os" "path" "testing" @@ -16,7 +16,7 @@ func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) { err := WriteToFile(tmpFilePath, content) assert.NoError(t, err) - fileContent, _ := ioutil.ReadFile(tmpFilePath) + fileContent, _ := os.ReadFile(tmpFilePath) assert.Equal(t, content, fileContent) } @@ -31,7 +31,7 @@ func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) { err = WriteToFile(tmpFilePath, content) assert.NoError(t, err) - fileContent, _ := ioutil.ReadFile(tmpFilePath) + fileContent, _ := os.ReadFile(tmpFilePath) assert.Equal(t, content, fileContent) } @@ -43,6 +43,6 @@ func Test_WriteFile_CanWriteANestedPath(t *testing.T) { err := WriteToFile(tmpFilePath, content) assert.NoError(t, err) - fileContent, _ := ioutil.ReadFile(tmpFilePath) + fileContent, _ := os.ReadFile(tmpFilePath) assert.Equal(t, content, fileContent) } diff --git a/api/git/azure.go b/api/git/azure.go index 7aa7b5097..6036fe5ac 100644 --- a/api/git/azure.go +++ b/api/git/azure.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -100,7 +99,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO if err != nil { return "", errors.WithMessage(err, "failed to build download url") } - zipFile, err := ioutil.TempFile("", "azure-git-repo-*.zip") + zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip") if err != nil { return "", errors.WithMessage(err, "failed to create temp file") } diff --git a/api/git/git_integration_test.go b/api/git/git_integration_test.go index 45504101e..886003c8d 100644 --- a/api/git/git_integration_test.go +++ b/api/git/git_integration_test.go @@ -274,7 +274,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) { func TestService_canStopCacheCleanTimer_whenContextDone(t *testing.T) { timeout := 10 * time.Millisecond - deadlineCtx, _ := context.WithDeadline(context.TODO(), time.Now().Add(10*timeout)) + deadlineCtx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(10*timeout)) + defer cancel() service := NewService(deadlineCtx) assert.False(t, service.timerHasStopped(), "timer should not be stopped") diff --git a/api/go.mod b/api/go.mod index 60d1da09d..43803a565 100644 --- a/api/go.mod +++ b/api/go.mod @@ -19,7 +19,7 @@ require ( github.com/go-ldap/ldap/v3 v3.1.8 github.com/go-playground/validator/v10 v10.10.1 github.com/gofrs/uuid v4.0.0+incompatible - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v4 v4.2.0 github.com/google/go-cmp v0.5.8 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.7.3 @@ -31,10 +31,11 @@ require ( github.com/json-iterator/go v1.1.12 github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a - github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 + github.com/portainer/libhelm v0.0.0-20221018213433-5ad83b50dbc9 github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777 github.com/robfig/cron/v3 v3.0.1 @@ -47,7 +48,7 @@ require ( golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.22.5 k8s.io/apimachinery v0.22.5 k8s.io/client-go v0.22.5 diff --git a/api/go.sum b/api/go.sum index 650360013..bd392a791 100644 --- a/api/go.sum +++ b/api/go.sum @@ -173,8 +173,8 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -352,6 +352,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -363,8 +365,10 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 h github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk= github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE= -github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04= -github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= +github.com/portainer/libhelm v0.0.0-20221017001602-b619e0ddf8fc h1:Ms8ZDGvSydX7aoCfu06Rk8II2N4tlX3cn7whtGC5fDA= +github.com/portainer/libhelm v0.0.0-20221017001602-b619e0ddf8fc/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= +github.com/portainer/libhelm v0.0.0-20221018213433-5ad83b50dbc9 h1:PMw+45hocpYsMQ7WLIlzsf4854BhDM8C631kr0DVJtc= +github.com/portainer/libhelm v0.0.0-20221018213433-5ad83b50dbc9/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a h1:BJ5V4EDNhg3ImYbmXnGS8vrMhq6rzsEneIXyJh0g4dc= github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a/go.mod h1:ckuHnoLA5kLuE5WkvPBXmrw63LUMdSH4aX71QRi9y10= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -747,6 +751,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/api/hostmanagement/openamt/authorization.go b/api/hostmanagement/openamt/authorization.go index 5e9d0773f..9d5050d2e 100644 --- a/api/hostmanagement/openamt/authorization.go +++ b/api/hostmanagement/openamt/authorization.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" portainer "github.com/portainer/portainer/api" @@ -33,7 +33,7 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati if err != nil { return "", err } - responseBody, readErr := ioutil.ReadAll(response.Body) + responseBody, readErr := io.ReadAll(response.Body) if readErr != nil { return "", readErr } diff --git a/api/hostmanagement/openamt/deviceFeatures.go b/api/hostmanagement/openamt/deviceFeatures.go index e71d96684..cce18a8db 100644 --- a/api/hostmanagement/openamt/deviceFeatures.go +++ b/api/hostmanagement/openamt/deviceFeatures.go @@ -21,9 +21,6 @@ func (service *Service) enableDeviceFeatures(configuration portainer.OpenAMTConf jsonValue, _ := json.Marshal(payload) _, err := service.executeSaveRequest(http.MethodPost, url, configuration.MPSToken, jsonValue) - if err != nil { - return err - } - return nil + return err } diff --git a/api/hostmanagement/openamt/openamt.go b/api/hostmanagement/openamt/openamt.go index ca03c999e..8c9902866 100644 --- a/api/hostmanagement/openamt/openamt.go +++ b/api/hostmanagement/openamt/openamt.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -83,11 +83,8 @@ func (service *Service) Configure(configuration portainer.OpenAMTConfiguration) } _, err = service.createOrUpdateDomain(configuration) - if err != nil { - return err - } - return nil + return err } func (service *Service) executeSaveRequest(method string, url string, token string, payload []byte) ([]byte, error) { @@ -102,7 +99,7 @@ func (service *Service) executeSaveRequest(method string, url string, token stri if err != nil { return nil, err } - responseBody, readErr := ioutil.ReadAll(response.Body) + responseBody, readErr := io.ReadAll(response.Body) if readErr != nil { return nil, readErr } @@ -131,7 +128,7 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err if err != nil { return nil, err } - responseBody, readErr := ioutil.ReadAll(response.Body) + responseBody, readErr := io.ReadAll(response.Body) if readErr != nil { return nil, readErr } @@ -229,12 +226,7 @@ func (service *Service) ExecuteDeviceAction(configuration portainer.OpenAMTConfi } configuration.MPSToken = token - err = service.executeDeviceAction(configuration, deviceGUID, int(parsedAction)) - if err != nil { - return err - } - - return nil + return service.executeDeviceAction(configuration, deviceGUID, int(parsedAction)) } func (service *Service) EnableDeviceFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string, features portainer.OpenAMTDeviceEnabledFeatures) (string, error) { diff --git a/api/http/client/client.go b/api/http/client/client.go index dcd070375..343e8fa2a 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -96,7 +96,7 @@ func Get(url string, timeout int) ([]byte, error) { return nil, errInvalidResponseStatus } - body, err := ioutil.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) if err != nil { return nil, err } diff --git a/api/http/handler/backup/backup_test.go b/api/http/handler/backup/backup_test.go index 8c2fb979f..a35754db3 100644 --- a/api/http/handler/backup/backup_test.go +++ b/api/http/handler/backup/backup_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -38,7 +37,7 @@ func listFiles(dir string) []string { func contains(t *testing.T, list []string, path string) { assert.Contains(t, list, path) - copyContent, _ := ioutil.ReadFile(path) + copyContent, _ := os.ReadFile(path) assert.Equal(t, "content\n", string(copyContent)) } @@ -58,7 +57,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T) tmpdir := t.TempDir() archivePath := filepath.Join(tmpdir, "archive.tar.gz") - err := ioutil.WriteFile(archivePath, body, 0600) + err := os.WriteFile(archivePath, body, 0600) if err != nil { t.Fatal("Failed to save downloaded .tar.gz archive: ", err) } diff --git a/api/http/handler/backup/handler.go b/api/http/handler/backup/handler.go index 500a3c374..cd95d71b8 100644 --- a/api/http/handler/backup/handler.go +++ b/api/http/handler/backup/handler.go @@ -6,7 +6,6 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/adminmonitor" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/demo" @@ -71,11 +70,3 @@ func adminAccess(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } - -func systemWasInitialized(dataStore dataservices.DataStore) (bool, error) { - users, err := dataStore.User().UsersByRole(portainer.AdministratorRole) - if err != nil { - return false, err - } - return len(users) > 0, nil -} diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index f5d69375c..9cab60c35 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -142,12 +142,7 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e return errors.New("Invalid note. tag is not supported") } - err := validateVariablesDefinitions(payload.Variables) - if err != nil { - return err - } - - return nil + return validateVariablesDefinitions(payload.Variables) } func isValidNote(note string) bool { @@ -249,12 +244,7 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) return errors.New("Invalid note. tag is not supported") } - err := validateVariablesDefinitions(payload.Variables) - if err != nil { - return err - } - - return nil + return validateVariablesDefinitions(payload.Variables) } func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) { @@ -397,16 +387,13 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er payload.FileContent = composeFileContent varsString, _ := request.RetrieveMultiPartFormValue(r, "Variables", true) - err = json.Unmarshal([]byte(varsString), &payload.Variables) - if err != nil { - return errors.New("Invalid variables. Ensure that the variables are valid JSON") + if varsString != "" { + err = json.Unmarshal([]byte(varsString), &payload.Variables) + if err != nil { + return errors.New("Invalid variables. Ensure that the variables are valid JSON") + } + return validateVariablesDefinitions(payload.Variables) } - - err = validateVariablesDefinitions(payload.Variables) - if err != nil { - return err - } - return nil } diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index 313145b4d..5aaac74bb 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -86,12 +86,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } - err := validateVariablesDefinitions(payload.Variables) - if err != nil { - return err - } - - return nil + return validateVariablesDefinitions(payload.Variables) } // @id CustomTemplateUpdate diff --git a/api/http/handler/customtemplates/handler.go b/api/http/handler/customtemplates/handler.go index c2f6f5391..7be8084a5 100644 --- a/api/http/handler/customtemplates/handler.go +++ b/api/http/handler/customtemplates/handler.go @@ -9,7 +9,6 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/http/security" - "github.com/portainer/portainer/api/internal/authorization" ) // Handler is the HTTP handler used to handle environment(endpoint) group operations. @@ -50,20 +49,3 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto func userCanEditTemplate(customTemplate *portainer.CustomTemplate, securityContext *security.RestrictedRequestContext) bool { return securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID } - -func userCanAccessTemplate(customTemplate portainer.CustomTemplate, securityContext *security.RestrictedRequestContext, resourceControl *portainer.ResourceControl) bool { - if securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID { - return true - } - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range securityContext.UserMemberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - - if resourceControl != nil && authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) { - return true - } - - return false -} diff --git a/api/http/handler/edgegroups/associated_endpoints.go b/api/http/handler/edgegroups/associated_endpoints.go index b34711eda..6c240083c 100644 --- a/api/http/handler/edgegroups/associated_endpoints.go +++ b/api/http/handler/edgegroups/associated_endpoints.go @@ -1,7 +1,7 @@ package edgegroups import ( - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type endpointSetType map[portainer.EndpointID]bool diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index cd41498f3..3a9096433 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -91,15 +91,13 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque } for _, tagID := range endpointGroup.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { - return httperror.InternalServerError("Unable to retrieve tag from the database", err) - } + handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + tag.EndpointGroups[endpointGroup.ID] = true + }) - tag.EndpointGroups[endpointGroup.ID] = true - - err = handler.DataStore.Tag().UpdateTag(tagID, tag) - if err != nil { + if handler.DataStore.IsErrObjectNotFound(err) { + return httperror.InternalServerError("Unable to find a tag inside the database", err) + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index feea30c53..a96bd7278 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -66,15 +66,13 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque } for _, tagID := range endpointGroup.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { - return httperror.InternalServerError("Unable to retrieve tag from the database", err) - } + handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + delete(tag.EndpointGroups, endpointGroup.ID) + }) - delete(tag.EndpointGroups, endpointGroup.ID) - - err = handler.DataStore.Tag().UpdateTag(tagID, tag) - if err != nil { + if handler.DataStore.IsErrObjectNotFound(err) { + return httperror.InternalServerError("Unable to find a tag inside the database", err) + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 8f9578f3c..fbbd3384a 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -81,28 +81,26 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet) for tagID := range removeTags { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { + handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + delete(tag.EndpointGroups, endpointGroup.ID) + }) + + if handler.DataStore.IsErrObjectNotFound(err) { return httperror.InternalServerError("Unable to find a tag inside the database", err) - } - delete(tag.EndpointGroups, endpointGroup.ID) - err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) - if err != nil { + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } endpointGroup.TagIDs = payload.TagIDs for _, tagID := range payload.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { + handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + tag.EndpointGroups[endpointGroup.ID] = true + }) + + if handler.DataStore.IsErrObjectNotFound(err) { return httperror.InternalServerError("Unable to find a tag inside the database", err) - } - - tag.EndpointGroups[endpointGroup.ID] = true - - err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) - if err != nil { + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a54dad51c..97858aa71 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -131,6 +131,17 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { return errors.New("Invalid Azure authentication key") } payload.AzureAuthenticationKey = azureAuthenticationKey + + case edgeAgentEnvironment: + endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", false) + if err != nil || strings.EqualFold("", strings.Trim(endpointURL, " ")) { + return errors.New("URL cannot be empty") + } + payload.URL = endpointURL + + publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) + payload.PublicURL = publicURL + default: endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { @@ -169,7 +180,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { // @produce json // @param Name formData string true "Name that will be used to identify this environment(endpoint) (example: my-environment)" // @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5) -// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" +// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)". Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment) // @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)" // @param GroupID formData int false "Environment(Endpoint) group identifier. If not specified will default to 1 (unassigned)." // @param TLS formData bool false "Require TLS to connect against this environment(endpoint)" @@ -530,14 +541,9 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer. } for _, tagID := range endpoint.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { - return err - } - - tag.Endpoints[endpoint.ID] = true - - err = handler.DataStore.Tag().UpdateTag(tagID, tag) + err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + tag.Endpoints[endpoint.ID] = true + }) if err != nil { return err } diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index e271fbbe7..45a840970 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -62,15 +62,13 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * } for _, tagID := range endpoint.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { + err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + delete(tag.Endpoints, endpoint.ID) + }) + + if handler.DataStore.IsErrObjectNotFound(err) { return httperror.NotFound("Unable to find tag inside the database", err) - } - - delete(tag.Endpoints, endpoint.ID) - - err = handler.DataStore.Tag().UpdateTag(tagID, tag) - if err != nil { + } else if err != nil { return httperror.InternalServerError("Unable to persist tag relation inside the database", err) } } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index d34d33428..a35f383ab 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -139,29 +139,26 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * removeTags := tag.Difference(endpointTagSet, payloadTagSet) for tagID := range removeTags { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { - return httperror.InternalServerError("Unable to find a tag inside the database", err) - } + err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + delete(tag.Endpoints, endpoint.ID) + }) - delete(tag.Endpoints, endpoint.ID) - err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) - if err != nil { + if handler.DataStore.IsErrObjectNotFound(err) { + return httperror.InternalServerError("Unable to find a tag inside the database", err) + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } endpoint.TagIDs = payload.TagIDs for _, tagID := range payload.TagIDs { - tag, err := handler.DataStore.Tag().Tag(tagID) - if err != nil { + err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) { + tag.Endpoints[endpoint.ID] = true + }) + + if handler.DataStore.IsErrObjectNotFound(err) { return httperror.InternalServerError("Unable to find a tag inside the database", err) - } - - tag.Endpoints[endpoint.ID] = true - - err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) - if err != nil { + } else if err != nil { return httperror.InternalServerError("Unable to persist tag changes inside the database", err) } } @@ -193,12 +190,8 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * switch *payload.Status { case 1: endpoint.Status = portainer.EndpointStatusUp - break case 2: endpoint.Status = portainer.EndpointStatusDown - break - default: - break } } @@ -331,7 +324,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * err = handler.SnapshotService.FillSnapshotData(endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to add snapshot data", err} + return httperror.InternalServerError("Unable to add snapshot data", err) } return response.JSON(w, endpoint) diff --git a/api/http/handler/helm/helm_install.go b/api/http/handler/helm/helm_install.go index 248e0659c..33f8f45da 100644 --- a/api/http/handler/helm/helm_install.go +++ b/api/http/handler/helm/helm_install.go @@ -2,7 +2,6 @@ package helm import ( "fmt" - "io/ioutil" "net/http" "os" "strings" @@ -192,7 +191,7 @@ func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte, for _, resource := range yamlResources { resource := resource // https://golang.org/doc/faq#closures_and_goroutines g.Go(func() error { - tmpfile, err := ioutil.TempFile("", "helm-manifest-*") + tmpfile, err := os.CreateTemp("", "helm-manifest-*") if err != nil { return errors.Wrap(err, "failed to create a tmp helm manifest file") } diff --git a/api/http/handler/hostmanagement/fdo/fdo.go b/api/http/handler/hostmanagement/fdo/fdo.go index ef846a648..69851f454 100644 --- a/api/http/handler/hostmanagement/fdo/fdo.go +++ b/api/http/handler/hostmanagement/fdo/fdo.go @@ -128,12 +128,7 @@ func (handler *Handler) addDefaultProfile() error { profile.FilePath = filePath profile.DateCreated = time.Now().Unix() - err = handler.DataStore.FDOProfile().Create(profile) - if err != nil { - return err - } - - return nil + return handler.DataStore.FDOProfile().Create(profile) } const defaultProfileFileContent = ` diff --git a/api/http/handler/hostmanagement/openamt/amtconfiguration.go b/api/http/handler/hostmanagement/openamt/amtconfiguration.go index 6ffe5667f..ff8db308b 100644 --- a/api/http/handler/hostmanagement/openamt/amtconfiguration.go +++ b/api/http/handler/hostmanagement/openamt/amtconfiguration.go @@ -169,12 +169,8 @@ func (handler *Handler) saveConfiguration(configuration portainer.OpenAMTConfigu configuration.MPSToken = "" settings.OpenAMTConfiguration = configuration - err = handler.DataStore.Settings().UpdateSettings(settings) - if err != nil { - return err - } - return nil + return handler.DataStore.Settings().UpdateSettings(settings) } func (handler *Handler) disableOpenAMT() error { diff --git a/api/http/handler/hostmanagement/openamt/amtrpc.go b/api/http/handler/hostmanagement/openamt/amtrpc.go index a4922d276..a0c2ccbf2 100644 --- a/api/http/handler/hostmanagement/openamt/amtrpc.go +++ b/api/http/handler/hostmanagement/openamt/amtrpc.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -139,7 +139,7 @@ func pullImage(ctx context.Context, docker *client.Client, imageName string) err } defer out.Close() - outputBytes, err := ioutil.ReadAll(out) + outputBytes, err := io.ReadAll(out) if err != nil { log.Error().Str("image_name", imageName).Err(err).Msg("could not read image pull output") @@ -261,7 +261,7 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain return "", err } - outputBytes, err := ioutil.ReadAll(out) + outputBytes, err := io.ReadAll(out) if err != nil { log.Error(). Str("image_name", imageName). @@ -295,29 +295,6 @@ func (handler *Handler) activateDevice(endpoint *portainer.Endpoint, settings po } _, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine) - if err != nil { - return err - } - return nil -} - -func (handler *Handler) deactivateDevice(endpoint *portainer.Endpoint, settings portainer.Settings) error { - ctx := context.TODO() - - config := settings.OpenAMTConfiguration - cmdLine := []string{ - "deactivate", - "-n", - "-v", - "-u", fmt.Sprintf("wss://%s/activate", config.MPSServer), - "-password", config.MPSPassword, - } - - _, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine) - if err != nil { - return err - } - - return nil + return err } diff --git a/api/http/handler/kubernetes/configmaps_and_secrets.go b/api/http/handler/kubernetes/configmaps_and_secrets.go index b38b16640..d1c7af22f 100644 --- a/api/http/handler/kubernetes/configmaps_and_secrets.go +++ b/api/http/handler/kubernetes/configmaps_and_secrets.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -9,7 +10,23 @@ import ( ) func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespace, err := request.RetrieveRouteVariableValue(r, "namespace") if err != nil { @@ -22,7 +39,7 @@ func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.R configmaps, err := cli.GetConfigMapsAndSecrets(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve configmaps and secrets", err, ) } diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 680e4ea46..f3f1a688b 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -3,6 +3,8 @@ package kubernetes import ( "errors" "net/http" + "net/url" + "strconv" portainer "github.com/portainer/portainer/api" portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors" @@ -24,7 +26,6 @@ type Handler struct { *mux.Router authorizationService *authorization.Service DataStore dataservices.DataStore - KubernetesClient portainer.KubeClient KubernetesClientFactory *cli.ClientFactory JwtService dataservices.JWTService kubeClusterAccessService kubernetes.KubeClusterAccessService @@ -39,7 +40,6 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz JwtService: jwtService, kubeClusterAccessService: kubeClusterAccessService, KubernetesClientFactory: kubernetesClientFactory, - KubernetesClient: kubernetesClient, } kubeRouter := h.PathPrefix("/kubernetes").Subrouter() @@ -62,6 +62,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut) endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet) endpointRouter.Path("/namespace/{namespace}").Handler(httperror.LoggerHandler(h.deleteKubernetesNamespaces)).Methods(http.MethodDelete) + endpointRouter.Path("/namespaces/{namespace}").Handler(httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet) // namespaces // in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?) @@ -85,13 +86,19 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) { endpoint, err := middlewares.FetchEndpoint(request) if err != nil { - httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err) + httperror.InternalServerError( + "Unable to find an environment on request context", + err, + ) return } if !endpointutils.IsKubernetesEndpoint(endpoint) { errMessage := "environment is not a Kubernetes environment" - httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage)) + httperror.BadRequest( + errMessage, + errors.New(errMessage), + ) return } @@ -109,6 +116,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Invalid environment identifier route variable", err, ) + return } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) @@ -119,6 +127,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Unable to find an environment with the specified identifier inside the database", err, ) + return } else if err != nil { httperror.WriteError( w, @@ -126,23 +135,101 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Unable to find an environment with the specified identifier inside the database", err, ) + return } if handler.KubernetesClientFactory == nil { next.ServeHTTP(w, r) return } - kubeCli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + // Generate a proxied kubeconfig, then create a kubeclient using it. + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + httperror.WriteError( + w, + http.StatusForbidden, + "Permission denied to access environment", + err, + ) + return + } + bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData) if err != nil { httperror.WriteError( w, http.StatusInternalServerError, - "Unable to create Kubernetes client", + "Unable to create JWT token", err, ) - + return } - handler.KubernetesClient = kubeCli + singleEndpointList := []portainer.Endpoint{ + *endpoint, + } + config, handlerErr := handler.buildConfig( + r, + tokenData, + bearerToken, + singleEndpointList, + ) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable to build endpoint kubeconfig", + handlerErr.Err, + ) + return + } + + if len(config.Clusters) == 0 { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable build cluster kubeconfig", + errors.New("Unable build cluster kubeconfig"), + ) + return + } + + // Manually setting the localhost to route + // the request to proxy server + serverURL, err := url.Parse(config.Clusters[0].Cluster.Server) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable parse cluster's kubeconfig server URL", + nil, + ) + return + } + serverURL.Scheme = "https" + serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS + config.Clusters[0].Cluster.Server = serverURL.String() + + yaml, err := cli.GenerateYAML(config) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable to generate yaml from endpoint kubeconfig", + err, + ) + return + } + kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml)) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Failed to create client from kubeconfig", + err, + ) + return + } + + handler.KubernetesClientFactory.SetProxyKubeClient(strconv.Itoa(int(endpoint.ID)), r.Header.Get("Authorization"), kubeCli) next.ServeHTTP(w, r) }) } diff --git a/api/http/handler/kubernetes/ingresses.go b/api/http/handler/kubernetes/ingresses.go index 0dfb8fadb..8af98df90 100644 --- a/api/http/handler/kubernetes/ingresses.go +++ b/api/http/handler/kubernetes/ingresses.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -49,12 +50,29 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r ) } - controllers := cli.GetIngressControllers() + controllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Failed to fetch ingressclasses", + err, + ) + } + + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + controllers = append(controllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } existingClasses := endpoint.Kubernetes.Configuration.IngressClasses var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { controllers[i].Availability = true - controllers[i].New = true + if controllers[i].ClassName != "none" { + controllers[i].New = true + } var updatedClass portainer.KubernetesIngressClassConfig updatedClass.Name = controllers[i].ClassName @@ -129,8 +147,31 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon ) } - cli := handler.KubernetesClient - currentControllers := cli.GetIngressControllers() + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + + currentControllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Failed to fetch ingressclasses", + err, + ) + } + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + currentControllers = append(currentControllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } kubernetesConfig := endpoint.Kubernetes.Configuration existingClasses := kubernetesConfig.IngressClasses ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace @@ -139,7 +180,9 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon for i := range currentControllers { var globallyblocked bool currentControllers[i].Availability = true - currentControllers[i].New = true + if currentControllers[i].ClassName != "none" { + currentControllers[i].New = true + } var updatedClass portainer.KubernetesIngressClassConfig updatedClass.Name = currentControllers[i].ClassName @@ -229,7 +272,21 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter } existingClasses := endpoint.Kubernetes.Configuration.IngressClasses - controllers := cli.GetIngressControllers() + controllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Unable to get ingress controllers", + err, + ) + } + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + controllers = append(controllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { controllers[i].Availability = true @@ -328,7 +385,7 @@ PayloadLoop: updatedClass.GloballyBlocked = existingClass.GloballyBlocked // Handle "allow" - if p.Availability == true { + if p.Availability { // remove the namespace from the list of blocked namespaces // in the existingClass. for _, blockedNS := range existingClass.BlockedNamespaces { @@ -401,11 +458,28 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + ingresses, err := cli.GetIngresses(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve ingresses", err, ) } @@ -431,11 +505,28 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.CreateIngress(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve the ingress", err, ) } @@ -443,10 +534,26 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R } func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sIngressDeleteRequests - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest("Invalid request payload", err) } @@ -454,7 +561,7 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http err = cli.DeleteIngresses(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete ingresses", err, ) } @@ -479,11 +586,28 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.UpdateIngress(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to update the ingress", err, ) } diff --git a/api/http/handler/kubernetes/namespaces.go b/api/http/handler/kubernetes/namespaces.go index 884ae7038..d2e4d0c17 100644 --- a/api/http/handler/kubernetes/namespaces.go +++ b/api/http/handler/kubernetes/namespaces.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -10,12 +11,28 @@ import ( ) func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespaces, err := cli.GetNamespaces() if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve namespaces", err, ) } @@ -23,11 +40,64 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R return response.JSON(w, namespaces) } +func (handler *Handler) getKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + + ns, err := request.RetrieveRouteVariableValue(r, "namespace") + if err != nil { + return httperror.BadRequest( + "Invalid namespace identifier route variable", + err, + ) + } + namespace, err := cli.GetNamespace(ns) + if err != nil { + return httperror.InternalServerError( + "Unable to retrieve namespace", + err, + ) + } + + return response.JSON(w, namespace) +} + func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sNamespaceDetails - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest( "Invalid request payload", @@ -38,7 +108,7 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http err = cli.CreateNamespace(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to create namespace", err, ) } @@ -46,7 +116,23 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http } func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespace, err := request.RetrieveRouteVariableValue(r, "namespace") if err != nil { @@ -59,7 +145,7 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt err = cli.DeleteNamespace(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete namespace", err, ) } @@ -68,17 +154,33 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt } func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sNamespaceDetails - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest("Invalid request payload", err) } err = cli.UpdateNamespace(payload) if err != nil { - return httperror.InternalServerError("Unable to retrieve nodes limits", err) + return httperror.InternalServerError("Unable to update namespace", err) } return nil } diff --git a/api/http/handler/kubernetes/services.go b/api/http/handler/kubernetes/services.go index 2e60513ce..0cc223eb9 100644 --- a/api/http/handler/kubernetes/services.go +++ b/api/http/handler/kubernetes/services.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -18,7 +19,24 @@ func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Req ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + services, err := cli.GetServices(namespace) if err != nil { return httperror.InternalServerError( @@ -48,11 +66,28 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.CreateService(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to create sercice", err, ) } @@ -60,10 +95,26 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R } func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sServiceDeleteRequests - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest( "Invalid request payload", @@ -74,7 +125,7 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http. err = cli.DeleteServices(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete service", err, ) } @@ -99,11 +150,27 @@ func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } err = cli.UpdateService(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to update service", err, ) } diff --git a/api/http/handler/registries/registry_create_test.go b/api/http/handler/registries/registry_create_test.go index 711a6ef36..515dc6c5e 100644 --- a/api/http/handler/registries/registry_create_test.go +++ b/api/http/handler/registries/registry_create_test.go @@ -1,15 +1,9 @@ package registries import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" "testing" portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/http/security" "github.com/stretchr/testify/assert" ) @@ -49,85 +43,3 @@ func Test_registryCreatePayload_Validate(t *testing.T) { assert.NoError(t, err) }) } - -type testRegistryService struct { - dataservices.RegistryService - createRegistry func(r *portainer.Registry) error - updateRegistry func(ID portainer.RegistryID, r *portainer.Registry) error - getRegistry func(ID portainer.RegistryID) (*portainer.Registry, error) -} - -type testDataStore struct { - dataservices.DataStore - registry *testRegistryService -} - -func (t testDataStore) Registry() dataservices.RegistryService { - return t.registry -} - -func (t testRegistryService) CreateRegistry(r *portainer.Registry) error { - return t.createRegistry(r) -} - -func (t testRegistryService) UpdateRegistry(ID portainer.RegistryID, r *portainer.Registry) error { - return t.updateRegistry(ID, r) -} - -func (t testRegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) { - return t.getRegistry(ID) -} - -func (t testRegistryService) Registries() ([]portainer.Registry, error) { - return nil, nil -} - -func (t testRegistryService) Create(registry *portainer.Registry) error { - return nil -} - -// Not entirely sure what this is intended to test -func deleteTestHandler_registryCreate(t *testing.T) { - payload := registryCreatePayload{ - Name: "Test registry", - Type: portainer.ProGetRegistry, - URL: "http://example.com", - BaseURL: "http://example.com", - Authentication: false, - Username: "username", - Password: "password", - Gitlab: portainer.GitlabRegistryData{}, - } - payloadBytes, err := json.Marshal(payload) - assert.NoError(t, err) - r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payloadBytes)) - w := httptest.NewRecorder() - - restrictedContext := &security.RestrictedRequestContext{ - IsAdmin: true, - UserID: portainer.UserID(1), - } - - ctx := security.StoreRestrictedRequestContext(r, restrictedContext) - r = r.WithContext(ctx) - - registry := portainer.Registry{} - handler := Handler{} - handler.DataStore = testDataStore{ - registry: &testRegistryService{ - createRegistry: func(r *portainer.Registry) error { - registry = *r - return nil - }, - }, - } - handlerError := handler.registryCreate(w, r) - assert.Nil(t, handlerError) - assert.Equal(t, payload.Name, registry.Name) - assert.Equal(t, payload.Type, registry.Type) - assert.Equal(t, payload.URL, registry.URL) - assert.Equal(t, payload.BaseURL, registry.BaseURL) - assert.Equal(t, payload.Authentication, registry.Authentication) - assert.Equal(t, payload.Username, registry.Username) - assert.Equal(t, payload.Password, registry.Password) -} diff --git a/api/http/handler/registries/registry_update_test.go b/api/http/handler/registries/registry_update_test.go deleted file mode 100644 index 59c28d695..000000000 --- a/api/http/handler/registries/registry_update_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package registries - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/security" - "github.com/stretchr/testify/assert" -) - -func ps(s string) *string { - return &s -} - -func pb(b bool) *bool { - return &b -} - -type TestBouncer struct{} - -func (t TestBouncer) AdminAccess(h http.Handler) http.Handler { - return h -} - -func (t TestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { - return h -} - -func (t TestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error { - return nil -} - -// TODO, no i don't know what this is actually intended to test either. -func delete_TestHandler_registryUpdate(t *testing.T) { - payload := registryUpdatePayload{ - Name: ps("Updated test registry"), - URL: ps("http://example.org/feed"), - BaseURL: ps("http://example.org"), - Authentication: pb(true), - Username: ps("username"), - Password: ps("password"), - } - payloadBytes, err := json.Marshal(payload) - assert.NoError(t, err) - registry := portainer.Registry{Type: portainer.ProGetRegistry, ID: 5} - r := httptest.NewRequest(http.MethodPut, "/registries/5", bytes.NewReader(payloadBytes)) - w := httptest.NewRecorder() - - restrictedContext := &security.RestrictedRequestContext{ - IsAdmin: true, - UserID: portainer.UserID(1), - } - - ctx := security.StoreRestrictedRequestContext(r, restrictedContext) - r = r.WithContext(ctx) - - updatedRegistry := portainer.Registry{} - handler := newHandler(nil) - handler.initRouter(TestBouncer{}) - handler.DataStore = testDataStore{ - registry: &testRegistryService{ - getRegistry: func(_ portainer.RegistryID) (*portainer.Registry, error) { - return ®istry, nil - }, - updateRegistry: func(ID portainer.RegistryID, r *portainer.Registry) error { - assert.Equal(t, ID, r.ID) - updatedRegistry = *r - return nil - }, - }, - } - - handler.ServeHTTP(w, r) - assert.Equal(t, http.StatusOK, w.Code) - // Registry type should remain intact - assert.Equal(t, registry.Type, updatedRegistry.Type) - - assert.Equal(t, *payload.Name, updatedRegistry.Name) - assert.Equal(t, *payload.URL, updatedRegistry.URL) - assert.Equal(t, *payload.BaseURL, updatedRegistry.BaseURL) - assert.Equal(t, *payload.Authentication, updatedRegistry.Authentication) - assert.Equal(t, *payload.Username, updatedRegistry.Username) - assert.Equal(t, *payload.Password, updatedRegistry.Password) -} diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index b3bd5741e..5eeb2e873 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -29,10 +29,7 @@ type resourceControlCreatePayload struct { SubResourceIDs []string `example:"617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08"` } -var ( - errResourceControlAlreadyExists = errors.New("A resource control is already applied on this resource") //http/resourceControl - errInvalidResourceControlType = errors.New("Unsupported resource control type") //http/resourceControl -) +var errResourceControlAlreadyExists = errors.New("A resource control is already applied on this resource") //http/resourceControl func (payload *resourceControlCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.ResourceID) { diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 49a005207..da5ee2d0c 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -251,12 +251,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error { settings.SnapshotInterval = snapshotInterval - err := handler.SnapshotService.SetSnapshotInterval(snapshotInterval) - if err != nil { - return err - } - - return nil + return handler.SnapshotService.SetSnapshotInterval(snapshotInterval) } func (handler *Handler) updateTLS(settings *portainer.Settings) *httperror.HandlerError { diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index ca447c903..d4d353978 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -3,7 +3,6 @@ package stacks import ( "context" "fmt" - "io/ioutil" "net/http" "os" "strconv" @@ -200,7 +199,7 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St //then process the remove operation if stack.IsComposeFormat { fileNames := stackutils.GetStackFilePaths(stack, false) - tmpDir, err := ioutil.TempDir("", "kube_delete") + tmpDir, err := os.MkdirTemp("", "kube_delete") if err != nil { return errors.Wrap(err, "failed to create temp directory for deleting kub stack") } diff --git a/api/http/handler/stacks/update_kubernetes_stack.go b/api/http/handler/stacks/update_kubernetes_stack.go index 0764c58d7..ed939addb 100644 --- a/api/http/handler/stacks/update_kubernetes_stack.go +++ b/api/http/handler/stacks/update_kubernetes_stack.go @@ -2,7 +2,6 @@ package stacks import ( "fmt" - "io/ioutil" "net/http" "os" "strconv" @@ -105,7 +104,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer. return httperror.BadRequest("Failed to retrieve user token data", err) } - tempFileDir, _ := ioutil.TempDir("", "kub_file_content") + tempFileDir, _ := os.MkdirTemp("", "kub_file_content") defer os.RemoveAll(tempFileDir) if err := filesystem.WriteToFile(filesystem.JoinPaths(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil { diff --git a/api/http/handler/templates/template_file.go b/api/http/handler/templates/template_file.go index 102a76644..3299f25cd 100644 --- a/api/http/handler/templates/template_file.go +++ b/api/http/handler/templates/template_file.go @@ -23,7 +23,7 @@ type filePayload struct { type fileResponse struct { // The requested file content - FileContent string `example: "version:2"` + FileContent string `example:"version:2"` } func (payload *filePayload) Validate(r *http.Request) error { diff --git a/api/http/handler/upload/handler.go b/api/http/handler/upload/handler.go index dd6a459a1..9c8cca2a4 100644 --- a/api/http/handler/upload/handler.go +++ b/api/http/handler/upload/handler.go @@ -2,7 +2,7 @@ package upload import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "net/http" diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 1925981ee..5fbec62a5 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -1,9 +1,10 @@ package users import ( - "github.com/portainer/portainer/api/dataservices/errors" "net/http" + "github.com/portainer/portainer/api/dataservices/errors" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go index 45cacfc73..eb0d8c030 100644 --- a/api/http/handler/webhooks/handler.go +++ b/api/http/handler/webhooks/handler.go @@ -1,12 +1,11 @@ package webhooks import ( - "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/internal/authorization" "net/http" + "github.com/portainer/portainer/api/dataservices" + httperror "github.com/portainer/libhttp/error" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/security" @@ -39,43 +38,3 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) return h } - -func (handler *Handler) checkResourceAccess(r *http.Request, resourceID string, resourceControlType portainer.ResourceControlType) *httperror.HandlerError { - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - return httperror.InternalServerError("Unable to retrieve user info from request context", err) - } - // non-admins - rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceID, resourceControlType) - if rc == nil || err != nil { - return httperror.InternalServerError("Unable to retrieve a resource control associated to the resource", err) - } - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range securityContext.UserMemberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - canAccess := authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, rc) - if !canAccess { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "This operation is disabled for non-admin users and unassigned access users"} - } - return nil -} - -func (handler *Handler) checkAuthorization(r *http.Request, endpoint *portainer.Endpoint, authorizations []portainer.Authorization) (bool, *httperror.HandlerError) { - err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) - if err != nil { - return false, httperror.Forbidden("Permission denied to access environment", err) - } - - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - return false, httperror.InternalServerError("Unable to retrieve user info from request context", err) - } - - authService := authorization.NewService(handler.DataStore) - isAdminOrAuthorized, err := authService.UserIsAdminOrAuthorized(securityContext.UserID, endpoint.ID, authorizations) - if err != nil { - return false, httperror.InternalServerError("Unable to get user authorizations", err) - } - return isAdminOrAuthorized, nil -} diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go index 0b7f64747..3596130f9 100644 --- a/api/http/handler/webhooks/webhook_delete.go +++ b/api/http/handler/webhooks/webhook_delete.go @@ -2,9 +2,10 @@ package webhooks import ( "errors" - "github.com/portainer/portainer/api/http/security" "net/http" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index 0168e231d..511448270 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -3,11 +3,12 @@ package webhooks import ( "context" "errors" - "github.com/portainer/portainer/api/internal/registryutils" "io" "net/http" "strings" + "github.com/portainer/portainer/api/internal/registryutils" + dockertypes "github.com/docker/docker/api/types" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go index 966611010..c2097a47d 100644 --- a/api/http/handler/webhooks/webhook_list.go +++ b/api/http/handler/webhooks/webhook_list.go @@ -1,9 +1,10 @@ package webhooks import ( - "github.com/portainer/portainer/api/http/security" "net/http" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index b4a5afb48..fcd9dcc8f 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -116,12 +116,7 @@ func hijackAttachStartOperation(websocketConn *websocket.Conn, endpoint *portain return err } - err = hijackRequest(websocketConn, httpConn, attachStartRequest) - if err != nil { - return err - } - - return nil + return hijackRequest(websocketConn, httpConn, attachStartRequest) } func createAttachStartRequest(attachID string) (*http.Request, error) { diff --git a/api/http/handler/websocket/dial_windows.go b/api/http/handler/websocket/dial_windows.go index e6d725a40..8f181e956 100644 --- a/api/http/handler/websocket/dial_windows.go +++ b/api/http/handler/websocket/dial_windows.go @@ -4,8 +4,9 @@ package websocket import ( - "github.com/Microsoft/go-winio" "net" + + "github.com/Microsoft/go-winio" ) func createDial(scheme, host string) (net.Conn, error) { diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index fb50403eb..ae95e99a0 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -121,12 +121,7 @@ func hijackExecStartOperation(websocketConn *websocket.Conn, endpoint *portainer return err } - err = hijackRequest(websocketConn, httpConn, execStartRequest) - if err != nil { - return err - } - - return nil + return hijackRequest(websocketConn, httpConn, execStartRequest) } func createExecStartRequest(execID string) (*http.Request, error) { diff --git a/api/http/handler/websocket/initdial.go b/api/http/handler/websocket/initdial.go index 327753d28..e9160ccf1 100644 --- a/api/http/handler/websocket/initdial.go +++ b/api/http/handler/websocket/initdial.go @@ -5,7 +5,7 @@ import ( "net" "net/url" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" ) diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go index 647399760..d1325cc03 100644 --- a/api/http/handler/websocket/proxy.go +++ b/api/http/handler/websocket/proxy.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/websocket" "github.com/koding/websocketproxy" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { diff --git a/api/http/handler/websocket/types.go b/api/http/handler/websocket/types.go index 614143aa0..97b3334f6 100644 --- a/api/http/handler/websocket/types.go +++ b/api/http/handler/websocket/types.go @@ -1,6 +1,6 @@ package websocket -import "github.com/portainer/portainer/api" +import portainer "github.com/portainer/portainer/api" type webSocketRequestParams struct { ID string diff --git a/api/http/proxy/factory/agent/transport.go b/api/http/proxy/factory/agent/transport.go index 4250f861d..8abe497eb 100644 --- a/api/http/proxy/factory/agent/transport.go +++ b/api/http/proxy/factory/agent/transport.go @@ -6,14 +6,11 @@ import ( portainer "github.com/portainer/portainer/api" ) -type ( - // Transport is an http.Transport wrapper that adds custom http headers to communicate to an Agent - Transport struct { - httpTransport *http.Transport - signatureService portainer.DigitalSignatureService - endpointIdentifier portainer.EndpointID - } -) +// Transport is an http.Transport wrapper that adds custom http headers to communicate to an Agent +type Transport struct { + httpTransport *http.Transport + signatureService portainer.DigitalSignatureService +} // NewTransport returns a new transport that can be used to send signed requests to a Portainer agent func NewTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *Transport { diff --git a/api/http/proxy/factory/azure/containergroup.go b/api/http/proxy/factory/azure/containergroup.go index d5eeb7a10..1f8ca581c 100644 --- a/api/http/proxy/factory/azure/containergroup.go +++ b/api/http/proxy/factory/azure/containergroup.go @@ -85,11 +85,8 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request) responseObject = decorateObject(responseObject, resourceControl) err = utils.RewriteResponse(response, responseObject, http.StatusOK) - if err != nil { - return response, err - } - return response, nil + return response, err } func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request) (*http.Response, error) { diff --git a/api/http/proxy/factory/docker/build.go b/api/http/proxy/factory/docker/build.go index 6b5dfa91f..27f3622d7 100644 --- a/api/http/proxy/factory/docker/build.go +++ b/api/http/proxy/factory/docker/build.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "errors" - "io/ioutil" + "io" "mime" "net/http" @@ -29,15 +29,19 @@ type postDockerfileRequest struct { func buildOperation(request *http.Request) error { contentTypeHeader := request.Header.Get("Content-Type") - mediaType, _, err := mime.ParseMediaType(contentTypeHeader) - if err != nil { - return err + mediaType := "" + if contentTypeHeader != "" { + var err error + mediaType, _, err = mime.ParseMediaType(contentTypeHeader) + if err != nil { + return err + } } var buffer []byte switch mediaType { case "": - body, err := ioutil.ReadAll(request.Body) + body, err := io.ReadAll(request.Body) if err != nil { return err } @@ -49,7 +53,8 @@ func buildOperation(request *http.Request) error { case "application/json": var req postDockerfileRequest - if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + err := json.NewDecoder(request.Body).Decode(&req) + if err != nil { return err } @@ -81,7 +86,7 @@ func buildOperation(request *http.Request) error { log.Info().Str("filename", hdr.Filename).Int64("size", hdr.Size).Msg("upload the file to build image") - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return err } @@ -105,7 +110,7 @@ func buildOperation(request *http.Request) error { return nil } - request.Body = ioutil.NopCloser(bytes.NewReader(buffer)) + request.Body = io.NopCloser(bytes.NewReader(buffer)) request.ContentLength = int64(len(buffer)) request.Header.Set("Content-Type", "application/x-tar") diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 573bfc21d..c137e2606 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "strings" @@ -190,7 +190,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - body, err := ioutil.ReadAll(request.Body) + body, err := io.ReadAll(request.Body) if err != nil { return nil, err } @@ -229,7 +229,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } } - request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + request.Body = io.NopCloser(bytes.NewBuffer(body)) } response, err := transport.executeDockerRequest(request) diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index 205c48c60..4df1d316a 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "github.com/docker/docker/api/types" @@ -116,7 +116,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque return nil, err } - body, err := ioutil.ReadAll(request.Body) + body, err := io.ReadAll(request.Body) if err != nil { return nil, err } @@ -135,7 +135,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque } } - request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + request.Body = io.NopCloser(bytes.NewBuffer(body)) } return transport.replaceRegistryAuthenticationHeader(request) diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 439988a96..86a252dae 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "path" "regexp" @@ -197,7 +197,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, r.Method = http.MethodPost - r.Body = ioutil.NopCloser(bytes.NewReader(newBody)) + r.Body = io.NopCloser(bytes.NewReader(newBody)) r.ContentLength = int64(len(newBody)) } diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go index 997898ea1..f2d620630 100644 --- a/api/http/proxy/factory/kubernetes/token.go +++ b/api/http/proxy/factory/kubernetes/token.go @@ -1,7 +1,7 @@ package kubernetes import ( - "io/ioutil" + "os" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" @@ -28,7 +28,7 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore dataservices.DataSt } if setLocalAdminToken { - token, err := ioutil.ReadFile(defaultServiceAccountTokenFile) + token, err := os.ReadFile(defaultServiceAccountTokenFile) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 86b793eed..1f05092f8 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "path" "regexp" @@ -189,7 +189,7 @@ func decorateAgentDockerHubRequest(r *http.Request, dataStore dataservices.DataS } r.Method = http.MethodPost - r.Body = ioutil.NopCloser(bytes.NewReader(newBody)) + r.Body = io.NopCloser(bytes.NewReader(newBody)) r.ContentLength = int64(len(newBody)) return nil diff --git a/api/http/proxy/factory/utils/json.go b/api/http/proxy/factory/utils/json.go index cab676834..7d12567e0 100644 --- a/api/http/proxy/factory/utils/json.go +++ b/api/http/proxy/factory/utils/json.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "mime" "gopkg.in/yaml.v3" @@ -50,7 +49,7 @@ func getBody(body io.ReadCloser, contentType string, isGzip bool) (interface{}, defer reader.Close() - bodyBytes, err := ioutil.ReadAll(reader) + bodyBytes, err := io.ReadAll(reader) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/utils/request.go b/api/http/proxy/factory/utils/request.go index 92724b7fa..9f809fc20 100644 --- a/api/http/proxy/factory/utils/request.go +++ b/api/http/proxy/factory/utils/request.go @@ -2,7 +2,7 @@ package utils import ( "bytes" - "io/ioutil" + "io" "net/http" "strconv" ) @@ -25,7 +25,7 @@ func RewriteRequest(request *http.Request, newData interface{}) error { return err } - body := ioutil.NopCloser(bytes.NewReader(data)) + body := io.NopCloser(bytes.NewReader(data)) request.Body = body request.ContentLength = int64(len(data)) diff --git a/api/http/proxy/factory/utils/response.go b/api/http/proxy/factory/utils/response.go index 674529883..868590295 100644 --- a/api/http/proxy/factory/utils/response.go +++ b/api/http/proxy/factory/utils/response.go @@ -3,7 +3,7 @@ package utils import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "strconv" @@ -82,7 +82,7 @@ func RewriteResponse(response *http.Response, newResponseData interface{}, statu return err } - body := ioutil.NopCloser(bytes.NewReader(data)) + body := io.NopCloser(bytes.NewReader(data)) response.StatusCode = statusCode response.Body = body diff --git a/api/http/security/rate_limiter.go b/api/http/security/rate_limiter.go index 65eea5d4a..58ad63315 100644 --- a/api/http/security/rate_limiter.go +++ b/api/http/security/rate_limiter.go @@ -29,7 +29,7 @@ func NewRateLimiter(maxRequests int, duration time.Duration, banDuration time.Du func (limiter *RateLimiter) LimitAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := StripAddrPort(r.RemoteAddr) - if banned := limiter.Inc(ip); banned == true { + if banned := limiter.Inc(ip); banned { httperror.WriteError(w, http.StatusForbidden, "Access denied", errors.ErrResourceAccessDenied) return } diff --git a/api/internal/edge/edgestack.go b/api/internal/edge/edgestack.go index 9a0837bc2..5e2aa520b 100644 --- a/api/internal/edge/edgestack.go +++ b/api/internal/edge/edgestack.go @@ -3,7 +3,7 @@ package edge import ( "errors" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) // EdgeStackRelatedEndpoints returns a list of environments(endpoints) related to this Edge stack diff --git a/api/internal/edge/endpoint.go b/api/internal/edge/endpoint.go index 492c803a7..843561107 100644 --- a/api/internal/edge/endpoint.go +++ b/api/internal/edge/endpoint.go @@ -1,6 +1,6 @@ package edge -import "github.com/portainer/portainer/api" +import portainer "github.com/portainer/portainer/api" // EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Environment(Endpoint) func EndpointRelatedEdgeStacks(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) []portainer.EdgeStackID { diff --git a/api/internal/ssl/ssl.go b/api/internal/ssl/ssl.go index 419b54ad0..f52b6bec5 100644 --- a/api/internal/ssl/ssl.go +++ b/api/internal/ssl/ssl.go @@ -166,10 +166,5 @@ func (service *Service) cacheInfo(certPath string, keyPath string, selfSigned bo settings.KeyPath = keyPath settings.SelfSigned = selfSigned - err = service.dataStore.SSLSettings().UpdateSettings(settings) - if err != nil { - return err - } - - return nil + return service.dataStore.SSLSettings().UpdateSettings(settings) } diff --git a/api/internal/tag/tag.go b/api/internal/tag/tag.go index 8dd5ad58b..ada130c88 100644 --- a/api/internal/tag/tag.go +++ b/api/internal/tag/tag.go @@ -1,6 +1,6 @@ package tag -import "github.com/portainer/portainer/api" +import portainer "github.com/portainer/portainer/api" type tagSet map[portainer.TagID]bool diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go index ec727e7e1..905cbfeee 100644 --- a/api/jwt/jwt.go +++ b/api/jwt/jwt.go @@ -9,7 +9,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/gorilla/securecookie" "github.com/rs/zerolog/log" ) diff --git a/api/jwt/jwt_kubeconfig_test.go b/api/jwt/jwt_kubeconfig_test.go index 107f27730..b3796b129 100644 --- a/api/jwt/jwt_kubeconfig_test.go +++ b/api/jwt/jwt_kubeconfig_test.go @@ -3,7 +3,7 @@ package jwt import ( "testing" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" i "github.com/portainer/portainer/api/internal/testhelpers" diff --git a/api/jwt/jwt_test.go b/api/jwt/jwt_test.go index 5a6a5c988..e98731eb2 100644 --- a/api/jwt/jwt_test.go +++ b/api/jwt/jwt_test.go @@ -1,12 +1,12 @@ package jwt import ( - i "github.com/portainer/portainer/api/internal/testhelpers" "testing" "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" portainer "github.com/portainer/portainer/api" + i "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" ) diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go index a510c73d8..729e3c801 100644 --- a/api/kubernetes/cli/access.go +++ b/api/kubernetes/cli/access.go @@ -117,9 +117,6 @@ func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]p configMap.Data[portainerConfigMapAccessPoliciesKey] = string(data) _, err = kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - if err != nil { - return err - } - return nil + return err } diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index 23bd59993..821a17a0e 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -5,8 +5,10 @@ import ( "net/http" "strconv" "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" @@ -23,6 +25,8 @@ type ( signatureService portainer.DigitalSignatureService instanceID string endpointClients cmap.ConcurrentMap + endpointProxyClients *cache.Cache + AddrHTTPS string } // KubeClient represent a service used to execute Kubernetes operations @@ -34,14 +38,24 @@ type ( ) // NewClientFactory returns a new instance of a ClientFactory -func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *ClientFactory { +func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*ClientFactory, error) { + if userSessionTimeout == "" { + userSessionTimeout = portainer.DefaultUserSessionTimeout + } + timeout, err := time.ParseDuration(userSessionTimeout) + if err != nil { + return nil, err + } + return &ClientFactory{ dataStore: dataStore, signatureService: signatureService, reverseTunnelService: reverseTunnelService, instanceID: instanceID, endpointClients: cmap.New(), - } + endpointProxyClients: cache.New(timeout, timeout), + AddrHTTPS: addrHTTPS, + }, nil } func (factory *ClientFactory) GetInstanceID() (instanceID string) { @@ -59,7 +73,7 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta key := strconv.Itoa(int(endpoint.ID)) client, ok := factory.endpointClients.Get(key) if !ok { - client, err := factory.createKubeClient(endpoint) + client, err := factory.createCachedAdminKubeClient(endpoint) if err != nil { return nil, err } @@ -71,7 +85,49 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta return client.(portainer.KubeClient), nil } -func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { +// 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) { + client, ok := factory.endpointProxyClients.Get(endpointID + "." + token) + if !ok { + return nil, false + } + return client.(portainer.KubeClient), true +} + +// SetProxyKubeClient stores a kubeclient in the cache. +func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli portainer.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) { + config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig)) + if err != nil { + return nil, err + } + cliConfig, err := config.ClientConfig() + if err != nil { + return nil, err + } + + cli, err := kubernetes.NewForConfig(cliConfig) + if err != nil { + return nil, err + } + + kubecli := &KubeClient{ + cli: cli, + instanceID: factory.instanceID, + lock: &sync.Mutex{}, + } + + return kubecli, nil +} + +func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { cli, err := factory.CreateClient(endpoint) if err != nil { return nil, err diff --git a/api/kubernetes/cli/ingress.go b/api/kubernetes/cli/ingress.go index ce7ffc442..cc46038bb 100644 --- a/api/kubernetes/cli/ingress.go +++ b/api/kubernetes/cli/ingress.go @@ -5,11 +5,12 @@ import ( "strings" "github.com/portainer/portainer/api/database/models" + "github.com/rs/zerolog/log" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { +func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) { var controllers []models.K8sIngressController // We know that each existing class points to a controller so we can start @@ -17,19 +18,22 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { classClient := kcl.cli.NetworkingV1().IngressClasses() classList, err := classClient.List(context.Background(), metav1.ListOptions{}) if err != nil { - return nil + return nil, err } // We want to know which of these controllers is in use. var ingresses []models.K8sIngressInfo namespaces, err := kcl.GetNamespaces() if err != nil { - return nil + return nil, err } for namespace := range namespaces { t, err := kcl.GetIngresses(namespace) if err != nil { - return nil + // User might not be able to list ingresses in system/not allowed + // namespaces. + log.Debug().Err(err).Msg("failed to list ingresses for the current user, skipped sending ingress") + continue } ingresses = append(ingresses, t...) } @@ -58,7 +62,7 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { } controllers = append(controllers, controller) } - return controllers + return controllers, nil } // GetIngresses gets all the ingresses for a given namespace in a k8s endpoint. @@ -86,11 +90,11 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, var infos []models.K8sIngressInfo for _, ingress := range ingressList.Items { - ingressClass := ingress.Spec.IngressClassName var info models.K8sIngressInfo info.Name = ingress.Name info.UID = string(ingress.UID) info.Namespace = namespace + ingressClass := ingress.Spec.IngressClassName info.ClassName = "" if ingressClass != nil { info.ClassName = *ingressClass @@ -109,6 +113,10 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, // Gather list of paths and hosts. hosts := make(map[string]struct{}) for _, r := range ingress.Spec.Rules { + // We collect all exiting hosts in a map to avoid duplicates. + // Then, later convert it to a slice for the frontend. + hosts[r.Host] = struct{}{} + if r.HTTP == nil { continue } @@ -120,12 +128,10 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, path.IngressName = info.Name path.Host = r.Host - // We collect all exiting hosts in a map to avoid duplicates. - // Then, later convert it to a slice for the frontend. - hosts[r.Host] = struct{}{} - path.Path = p.Path - path.PathType = string(*p.PathType) + if p.PathType != nil { + path.PathType = string(*p.PathType) + } path.ServiceName = p.Backend.Service.Name path.Port = int(p.Backend.Service.Port.Number) info.Paths = append(info.Paths, path) @@ -150,7 +156,9 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf ingress.Name = info.Name ingress.Namespace = info.Namespace - ingress.Spec.IngressClassName = &info.ClassName + if info.ClassName != "" { + ingress.Spec.IngressClassName = &info.ClassName + } ingress.Annotations = info.Annotations // Store TLS information. @@ -220,7 +228,9 @@ func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInf ingress.Name = info.Name ingress.Namespace = info.Namespace - ingress.Spec.IngressClassName = &info.ClassName + if info.ClassName != "" { + ingress.Spec.IngressClassName = &info.ClassName + } ingress.Annotations = info.Annotations // Store TLS information. diff --git a/api/kubernetes/cli/namespace.go b/api/kubernetes/cli/namespace.go index 18701822d..0baacf09a 100644 --- a/api/kubernetes/cli/namespace.go +++ b/api/kubernetes/cli/namespace.go @@ -44,6 +44,21 @@ func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, e return results, nil } +// GetNamespace gets the namespace in the current k8s environment(endpoint). +func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) { + namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return portainer.K8sNamespaceInfo{}, err + } + + result := portainer.K8sNamespaceInfo{ + IsSystem: isSystemNamespace(*namespace), + IsDefault: namespace.Name == defaultNamespace, + } + + return result, nil +} + // CreateIngress creates a new ingress in a given namespace in a k8s endpoint. func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error { client := kcl.cli.CoreV1().Namespaces() diff --git a/api/kubernetes/cli/nodes_limits_test.go b/api/kubernetes/cli/nodes_limits_test.go index bf880c2ff..25ebe56a4 100644 --- a/api/kubernetes/cli/nodes_limits_test.go +++ b/api/kubernetes/cli/nodes_limits_test.go @@ -1,14 +1,15 @@ package cli import ( + "reflect" + "testing" + portainer "github.com/portainer/portainer/api" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" kfake "k8s.io/client-go/kubernetes/fake" - "reflect" - "testing" ) func newNodes() *v1.NodeList { diff --git a/api/kubernetes/cli/resource_test.go b/api/kubernetes/cli/resource_test.go index 902671103..d86044e98 100644 --- a/api/kubernetes/cli/resource_test.go +++ b/api/kubernetes/cli/resource_test.go @@ -85,7 +85,7 @@ func Test_GenerateYAML(t *testing.T) { t.Errorf("generateYamlConfig failed; err=%s", err) } - if compareYAMLStrings(yaml, ryt.wantYAML) != 0 { + if compareYAMLStrings(string(yaml), ryt.wantYAML) != 0 { t.Errorf("generateYamlConfig failed;\ngot=\n%s\nwant=\n%s", yaml, ryt.wantYAML) } }) diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go index 4bf7f58fc..80fc825e2 100644 --- a/api/kubernetes/cli/role.go +++ b/api/kubernetes/cli/role.go @@ -25,6 +25,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule { Resources: []string{"namespaces", "pods", "nodes"}, APIGroups: []string{"metrics.k8s.io"}, }, + { + Verbs: []string{"list"}, + Resources: []string{"ingressclasses"}, + APIGroups: []string{"networking.k8s.io"}, + }, } } diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go index f9c421ec6..a50620b4d 100644 --- a/api/kubernetes/cli/service_account.go +++ b/api/kubernetes/cli/service_account.go @@ -37,7 +37,7 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) // SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes // cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. -//It will also create required default RoleBinding and ClusterRoleBinding rules. +// It will also create required default RoleBinding and ClusterRoleBinding rules. func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error { serviceAccountName := UserServiceAccountName(userID, kcl.instanceID) diff --git a/api/kubernetes/kubeclusteraccess_service.go b/api/kubernetes/kubeclusteraccess_service.go index 0b1e785d5..4a97885d4 100644 --- a/api/kubernetes/kubeclusteraccess_service.go +++ b/api/kubernetes/kubeclusteraccess_service.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "encoding/pem" "fmt" - "io/ioutil" + "os" "strings" portainer "github.com/portainer/portainer/api" @@ -63,7 +63,7 @@ func getCertificateAuthorityData(tlsCertPath string) (string, error) { return "", errTLSCertNotProvided } - data, err := ioutil.ReadFile(tlsCertPath) + data, err := os.ReadFile(tlsCertPath) if err != nil { return "", errors.Wrap(errTLSCertFileMissing, err.Error()) } @@ -106,7 +106,7 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/")) } - log.Info(). + log.Debug(). Str("host_URL", hostURL). Str("HTTPS_bind_address", service.httpsBindAddr). Str("base_URL", baseURL). diff --git a/api/kubernetes/kubeclusteraccess_service_test.go b/api/kubernetes/kubeclusteraccess_service_test.go index 51f421668..71ce50f30 100644 --- a/api/kubernetes/kubeclusteraccess_service_test.go +++ b/api/kubernetes/kubeclusteraccess_service_test.go @@ -2,7 +2,7 @@ package kubernetes import ( "fmt" - "io/ioutil" + "os" "strings" "testing" @@ -38,7 +38,7 @@ const certDataString = "MIIC5TCCAc2gAwIBAgIJAJ+poiEBdsplMA0GCSqGSIb3DQEBCwUAMBQx func createTempFile(filename, content string, t *testing.T) string { tempPath := t.TempDir() filePath := fmt.Sprintf("%s/%s", tempPath, filename) - ioutil.WriteFile(filePath, []byte(content), 0644) + os.WriteFile(filePath, []byte(content), 0644) return filePath } diff --git a/api/oauth/oauth.go b/api/oauth/oauth.go index 3088ca821..9843258b2 100644 --- a/api/oauth/oauth.go +++ b/api/oauth/oauth.go @@ -3,7 +3,7 @@ package oauth import ( "context" "encoding/json" - "io/ioutil" + "io" "mime" "net/http" "net/url" @@ -11,7 +11,7 @@ import ( portainer "github.com/portainer/portainer/api" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/oauth2" @@ -127,7 +127,7 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/api/portainer.go b/api/portainer.go index d7b33075a..55551e6bb 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -130,6 +130,7 @@ type ( MaxBatchDelay *time.Duration SecretKeyName *string LogLevel *string + LogMode *string } // CustomTemplateVariableDefinition @@ -557,6 +558,7 @@ type ( IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"` IngressAvailabilityPerNamespace bool `json:"IngressAvailabilityPerNamespace"` + AllowNoneIngressClass bool `json:"AllowNoneIngressClass"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration @@ -1358,9 +1360,10 @@ type ( CreateNamespace(info models.K8sNamespaceDetails) error UpdateNamespace(info models.K8sNamespaceDetails) error GetNamespaces() (map[string]K8sNamespaceInfo, error) + GetNamespace(string) (K8sNamespaceInfo, error) DeleteNamespace(namespace string) error GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) - GetIngressControllers() models.K8sIngressControllers + GetIngressControllers() (models.K8sIngressControllers, error) CreateIngress(namespace string, info models.K8sIngressInfo) error UpdateIngress(namespace string, info models.K8sIngressInfo) error GetIngresses(namespace string) ([]models.K8sIngressInfo, error) diff --git a/api/stacks/deployments/deployment_kubernetes_config.go b/api/stacks/deployments/deployment_kubernetes_config.go index faf974a8c..b94d6c4b4 100644 --- a/api/stacks/deployments/deployment_kubernetes_config.go +++ b/api/stacks/deployments/deployment_kubernetes_config.go @@ -2,7 +2,6 @@ package deployments import ( "fmt" - "io/ioutil" "os" "github.com/pkg/errors" @@ -41,7 +40,7 @@ func (config *KubernetesStackDeploymentConfig) Deploy() error { manifestFilePaths := make([]string, len(fileNames)) - tmpDir, err := ioutil.TempDir("", "kub_deployment") + tmpDir, err := os.MkdirTemp("", "kub_deployment") if err != nil { return errors.Wrap(err, "failed to create temp kub deployment directory") } @@ -50,7 +49,7 @@ func (config *KubernetesStackDeploymentConfig) Deploy() error { for _, fileName := range fileNames { manifestFilePath := filesystem.JoinPaths(tmpDir, fileName) - manifestContent, err := ioutil.ReadFile(filesystem.JoinPaths(config.stack.ProjectPath, fileName)) + manifestContent, err := os.ReadFile(filesystem.JoinPaths(config.stack.ProjectPath, fileName)) if err != nil { return errors.Wrap(err, "failed to read manifest file") } diff --git a/api/stacks/stackutils/validation.go b/api/stacks/stackutils/validation.go index e5ffe564d..daace6e60 100644 --- a/api/stacks/stackutils/validation.go +++ b/api/stacks/stackutils/validation.go @@ -43,7 +43,7 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo } } - if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { + if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged { return errors.New("privileged mode disabled for non administrator users") } diff --git a/app/assets/css/icon.css b/app/assets/css/icon.css index c2da0ea15..cee731f05 100644 --- a/app/assets/css/icon.css +++ b/app/assets/css/icon.css @@ -102,6 +102,9 @@ pr-icon { } .icon-nested-blue { + @apply bg-blue-3 text-blue-8; + @apply th-dark:bg-gray-9 th-dark:text-blue-3; + @apply th-highcontrast:bg-gray-9 th-highcontrast:text-blue-3; display: flex; justify-content: center; align-items: center; @@ -110,7 +113,6 @@ pr-icon { padding: 5px; text-align: center; border-radius: 50%; - background-color: var(--ui-blue-3); margin-right: 5px; } diff --git a/app/azure/index.ts b/app/azure/index.ts index cb005b21a..a48d33f2f 100644 --- a/app/azure/index.ts +++ b/app/azure/index.ts @@ -1,7 +1,7 @@ import angular from 'angular'; import { StateRegistry, StateService } from '@uirouter/angularjs'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { notifyError } from '@/portainer/services/notifications'; import { EndpointProvider, StateManager } from '@/portainer/services/types'; diff --git a/app/docker/__module.js b/app/docker/__module.js index fc94883f4..59f613677 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -1,6 +1,6 @@ import angular from 'angular'; -import { EnvironmentStatus } from '@/portainer/environments/types'; +import { EnvironmentStatus } from '@/react/portainer/environments/types'; import { reactModule } from './react'; diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html index 61b0ccb24..2f3ac1f15 100644 --- a/app/docker/components/log-viewer/logViewer.html +++ b/app/docker/components/log-viewer/logViewer.html @@ -86,7 +86,7 @@
-      

{{ span.text }}

+

{{ span.text }}

No log line matching the '{{ $ctrl.state.search }}' filter

No logs available

diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index 4cee6ec92..07b47267e 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -1,6 +1,7 @@ import moment from 'moment'; -import _ from 'lodash-es'; + import { NEW_LINE_BREAKER } from '@/constants'; +import { concatLogsToString } from '@/docker/helpers/logHelper'; angular.module('portainer.docker').controller('LogViewerController', [ '$scope', @@ -74,9 +75,8 @@ angular.module('portainer.docker').controller('LogViewerController', [ }; this.downloadLogs = function () { - // To make the feature of downloading container logs working both on Windows and Linux, - // we need to use correct new line breakers on corresponding OS. - const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + log.line + NEW_LINE_BREAKER, '')]); + const logsAsString = concatLogsToString(this.state.filteredLogs); + const data = new Blob([logsAsString]); FileSaver.saveAs(data, this.resourceName + '_logs.txt'); }; }, diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js deleted file mode 100644 index d2214b3c2..000000000 --- a/app/docker/helpers/logHelper.js +++ /dev/null @@ -1,223 +0,0 @@ -import tokenize from '@nxmix/tokenize-ansi'; -import x256 from 'x256'; -import { takeRight, without } from 'lodash'; -import { format } from 'date-fns'; - -const FOREGROUND_COLORS_BY_ANSI = { - black: x256.colors[0], - red: x256.colors[1], - green: x256.colors[2], - yellow: x256.colors[3], - blue: x256.colors[4], - magenta: x256.colors[5], - cyan: x256.colors[6], - white: x256.colors[7], - brightBlack: x256.colors[8], - brightRed: x256.colors[9], - brightGreen: x256.colors[10], - brightYellow: x256.colors[11], - brightBlue: x256.colors[12], - brightMagenta: x256.colors[13], - brightCyan: x256.colors[14], - brightWhite: x256.colors[15], -}; - -const BACKGROUND_COLORS_BY_ANSI = { - bgBlack: x256.colors[0], - bgRed: x256.colors[1], - bgGreen: x256.colors[2], - bgYellow: x256.colors[3], - bgBlue: x256.colors[4], - bgMagenta: x256.colors[5], - bgCyan: x256.colors[6], - bgWhite: x256.colors[7], - bgBrightBlack: x256.colors[8], - bgBrightRed: x256.colors[9], - bgBrightGreen: x256.colors[10], - bgBrightYellow: x256.colors[11], - bgBrightBlue: x256.colors[12], - bgBrightMagenta: x256.colors[13], - bgBrightCyan: x256.colors[14], - bgBrightWhite: x256.colors[15], -}; - -const TIMESTAMP_LENGTH = 31; // 30 for timestamp + 1 for trailing space - -angular.module('portainer.docker').factory('LogHelper', [ - function LogHelperFactory() { - 'use strict'; - var helper = {}; - - function stripHeaders(logs) { - logs = logs.substring(8); - logs = logs.replace(/\r?\n(.{8})/g, '\n'); - - return logs; - } - - function stripEscapeCodes(logs) { - return logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); - } - - function cssColorFromRgb(rgb) { - const [r, g, b] = rgb; - - return `rgb(${r}, ${g}, ${b})`; - } - - function extendedColorForToken(token) { - const colorMode = token[1]; - - if (colorMode === 2) { - return cssColorFromRgb(token.slice(2)); - } - - if (colorMode === 5 && x256.colors[token[2]]) { - return cssColorFromRgb(x256.colors[token[2]]); - } - - return ''; - } - - // Return an array with each log including a line and styled spans for each entry. - // If the stripHeaders param is specified, it will strip the 8 first characters of each line. - // withTimestamps param is needed to find the start of JSON for Zerolog logs parsing - helper.formatLogs = function (logs, { stripHeaders: skipHeaders, withTimestamps }) { - if (skipHeaders) { - logs = stripHeaders(logs); - } - - const tokens = tokenize(logs); - const formattedLogs = []; - - let foregroundColor = null; - let backgroundColor = null; - let line = ''; - let spans = []; - - for (const token of tokens) { - const type = token[0]; - - if (FOREGROUND_COLORS_BY_ANSI[type]) { - foregroundColor = cssColorFromRgb(FOREGROUND_COLORS_BY_ANSI[type]); - } else if (type === 'moreColor') { - foregroundColor = extendedColorForToken(token); - } else if (type === 'fgDefault') { - foregroundColor = null; - } else if (BACKGROUND_COLORS_BY_ANSI[type]) { - backgroundColor = cssColorFromRgb(BACKGROUND_COLORS_BY_ANSI[type]); - } else if (type === 'bgMoreColor') { - backgroundColor = extendedColorForToken(token); - } else if (type === 'bgDefault') { - backgroundColor = null; - } else if (type === 'reset') { - foregroundColor = null; - backgroundColor = null; - } else if (type === 'text') { - const tokenLines = token[1].split('\n'); - - for (let i = 0; i < tokenLines.length; i++) { - if (i !== 0) { - formattedLogs.push({ line, spans }); - - line = ''; - spans = []; - } - - const text = stripEscapeCodes(tokenLines[i]); - if ((!withTimestamps && text.startsWith('{')) || (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))) { - line += JSONToFormattedLine(text, spans, withTimestamps); - } else { - spans.push({ foregroundColor, backgroundColor, text }); - line += text; - } - } - } - } - - if (line) { - formattedLogs.push({ line, spans }); - } - - return formattedLogs; - }; - - return helper; - }, -]); - -const JSONColors = { - Grey: 'var(--text-log-viewer-color-json-grey)', - Magenta: 'var(--text-log-viewer-color-json-magenta)', - Yellow: 'var(--text-log-viewer-color-json-yellow)', - Green: 'var(--text-log-viewer-color-json-green)', - Red: 'var(--text-log-viewer-color-json-red)', - Blue: 'var(--text-log-viewer-color-json-blue)', -}; - -const spaceSpan = { text: ' ' }; - -function logLevelToSpan(level) { - switch (level) { - case 'debug': - return { foregroundColor: JSONColors.Grey, text: 'DBG', fontWeight: 'bold' }; - case 'info': - return { foregroundColor: JSONColors.Green, text: 'INF', fontWeight: 'bold' }; - case 'warn': - return { foregroundColor: JSONColors.Yellow, text: 'WRN', fontWeight: 'bold' }; - case 'error': - return { foregroundColor: JSONColors.Red, text: 'ERR', fontWeight: 'bold' }; - default: - return { text: level }; - } -} - -function JSONToFormattedLine(rawText, spans, withTimestamps) { - const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText; - const json = JSON.parse(text); - const { level, caller, message, time } = json; - let line = ''; - - if (withTimestamps) { - const timestamp = rawText.substring(0, TIMESTAMP_LENGTH); - spans.push({ text: timestamp }); - line += `${timestamp}`; - } - if (time) { - const date = format(new Date(time * 1000), 'Y/MM/dd hh:mmaa'); - spans.push({ foregroundColor: JSONColors.Grey, text: date }, spaceSpan); - line += `${date} `; - } - if (level) { - const levelSpan = logLevelToSpan(level); - spans.push(levelSpan, spaceSpan); - line += `${levelSpan.text} `; - } - if (caller) { - const trimmedCaller = takeRight(caller.split('/'), 2).join('/'); - spans.push({ foregroundColor: JSONColors.Magenta, text: trimmedCaller, fontWeight: 'bold' }, spaceSpan); - spans.push({ foregroundColor: JSONColors.Blue, text: '>' }, spaceSpan); - line += `${trimmedCaller} > `; - } - - const keys = without(Object.keys(json), 'time', 'level', 'caller', 'message'); - if (message) { - spans.push({ foregroundColor: JSONColors.Magenta, text: `${message}` }, spaceSpan); - line += `${message} `; - - if (keys.length) { - spans.push({ foregroundColor: JSONColors.Magenta, text: `|` }, spaceSpan); - line += '| '; - } - } - - keys.forEach((key) => { - const value = json[key]; - spans.push({ foregroundColor: JSONColors.Blue, text: `${key}=` }); - spans.push({ foregroundColor: key === 'error' ? JSONColors.Red : JSONColors.Magenta, text: value }); - spans.push(spaceSpan); - line += `${key}=${value} `; - }); - - return line; -} diff --git a/app/docker/helpers/logHelper/colors/colors.ts b/app/docker/helpers/logHelper/colors/colors.ts new file mode 100644 index 000000000..f9183c2b6 --- /dev/null +++ b/app/docker/helpers/logHelper/colors/colors.ts @@ -0,0 +1,63 @@ +// original code comes from https://www.npmjs.com/package/x256 +// only picking the used parts as there is no type definition +// package is unmaintained and repository doesn't exist anymore + +// colors scraped from +// http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html +// %s/ *\d\+ \+#\([^ ]\+\)/\1\r/g + +import rawColors from './rawColors.json'; + +export type RGBColor = [number, number, number]; +export type TextColor = string | undefined; + +function hexToRGB(hex: string): RGBColor { + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return [r, g, b]; +} + +export const colors = rawColors.map(hexToRGB); + +export const FOREGROUND_COLORS_BY_ANSI: { + [k: string]: RGBColor; +} = { + black: colors[0], + red: colors[1], + green: colors[2], + yellow: colors[3], + blue: colors[4], + magenta: colors[5], + cyan: colors[6], + white: colors[7], + brightBlack: colors[8], + brightRed: colors[9], + brightGreen: colors[10], + brightYellow: colors[11], + brightBlue: colors[12], + brightMagenta: colors[13], + brightCyan: colors[14], + brightWhite: colors[15], +}; + +export const BACKGROUND_COLORS_BY_ANSI: { + [k: string]: RGBColor; +} = { + bgBlack: colors[0], + bgRed: colors[1], + bgGreen: colors[2], + bgYellow: colors[3], + bgBlue: colors[4], + bgMagenta: colors[5], + bgCyan: colors[6], + bgWhite: colors[7], + bgBrightBlack: colors[8], + bgBrightRed: colors[9], + bgBrightGreen: colors[10], + bgBrightYellow: colors[11], + bgBrightBlue: colors[12], + bgBrightMagenta: colors[13], + bgBrightCyan: colors[14], + bgBrightWhite: colors[15], +}; diff --git a/app/docker/helpers/logHelper/colors/index.ts b/app/docker/helpers/logHelper/colors/index.ts new file mode 100644 index 000000000..25eed2280 --- /dev/null +++ b/app/docker/helpers/logHelper/colors/index.ts @@ -0,0 +1,7 @@ +export { + type RGBColor, + type TextColor, + colors, + FOREGROUND_COLORS_BY_ANSI, + BACKGROUND_COLORS_BY_ANSI, +} from './colors'; diff --git a/app/docker/helpers/logHelper/colors/rawColors.json b/app/docker/helpers/logHelper/colors/rawColors.json new file mode 100644 index 000000000..b9bba82d4 --- /dev/null +++ b/app/docker/helpers/logHelper/colors/rawColors.json @@ -0,0 +1,258 @@ +[ + "000000", + "800000", + "008000", + "808000", + "000080", + "800080", + "008080", + "c0c0c0", + "808080", + "ff0000", + "00ff00", + "ffff00", + "0000ff", + "ff00ff", + "00ffff", + "ffffff", + "000000", + "00005f", + "000087", + "0000af", + "0000d7", + "0000ff", + "005f00", + "005f5f", + "005f87", + "005faf", + "005fd7", + "005fff", + "008700", + "00875f", + "008787", + "0087af", + "0087d7", + "0087ff", + "00af00", + "00af5f", + "00af87", + "00afaf", + "00afd7", + "00afff", + "00d700", + "00d75f", + "00d787", + "00d7af", + "00d7d7", + "00d7ff", + "00ff00", + "00ff5f", + "00ff87", + "00ffaf", + "00ffd7", + "00ffff", + "5f0000", + "5f005f", + "5f0087", + "5f00af", + "5f00d7", + "5f00ff", + "5f5f00", + "5f5f5f", + "5f5f87", + "5f5faf", + "5f5fd7", + "5f5fff", + "5f8700", + "5f875f", + "5f8787", + "5f87af", + "5f87d7", + "5f87ff", + "5faf00", + "5faf5f", + "5faf87", + "5fafaf", + "5fafd7", + "5fafff", + "5fd700", + "5fd75f", + "5fd787", + "5fd7af", + "5fd7d7", + "5fd7ff", + "5fff00", + "5fff5f", + "5fff87", + "5fffaf", + "5fffd7", + "5fffff", + "870000", + "87005f", + "870087", + "8700af", + "8700d7", + "8700ff", + "875f00", + "875f5f", + "875f87", + "875faf", + "875fd7", + "875fff", + "878700", + "87875f", + "878787", + "8787af", + "8787d7", + "8787ff", + "87af00", + "87af5f", + "87af87", + "87afaf", + "87afd7", + "87afff", + "87d700", + "87d75f", + "87d787", + "87d7af", + "87d7d7", + "87d7ff", + "87ff00", + "87ff5f", + "87ff87", + "87ffaf", + "87ffd7", + "87ffff", + "af0000", + "af005f", + "af0087", + "af00af", + "af00d7", + "af00ff", + "af5f00", + "af5f5f", + "af5f87", + "af5faf", + "af5fd7", + "af5fff", + "af8700", + "af875f", + "af8787", + "af87af", + "af87d7", + "af87ff", + "afaf00", + "afaf5f", + "afaf87", + "afafaf", + "afafd7", + "afafff", + "afd700", + "afd75f", + "afd787", + "afd7af", + "afd7d7", + "afd7ff", + "afff00", + "afff5f", + "afff87", + "afffaf", + "afffd7", + "afffff", + "d70000", + "d7005f", + "d70087", + "d700af", + "d700d7", + "d700ff", + "d75f00", + "d75f5f", + "d75f87", + "d75faf", + "d75fd7", + "d75fff", + "d78700", + "d7875f", + "d78787", + "d787af", + "d787d7", + "d787ff", + "d7af00", + "d7af5f", + "d7af87", + "d7afaf", + "d7afd7", + "d7afff", + "d7d700", + "d7d75f", + "d7d787", + "d7d7af", + "d7d7d7", + "d7d7ff", + "d7ff00", + "d7ff5f", + "d7ff87", + "d7ffaf", + "d7ffd7", + "d7ffff", + "ff0000", + "ff005f", + "ff0087", + "ff00af", + "ff00d7", + "ff00ff", + "ff5f00", + "ff5f5f", + "ff5f87", + "ff5faf", + "ff5fd7", + "ff5fff", + "ff8700", + "ff875f", + "ff8787", + "ff87af", + "ff87d7", + "ff87ff", + "ffaf00", + "ffaf5f", + "ffaf87", + "ffafaf", + "ffafd7", + "ffafff", + "ffd700", + "ffd75f", + "ffd787", + "ffd7af", + "ffd7d7", + "ffd7ff", + "ffff00", + "ffff5f", + "ffff87", + "ffffaf", + "ffffd7", + "ffffff", + "080808", + "121212", + "1c1c1c", + "262626", + "303030", + "3a3a3a", + "444444", + "4e4e4e", + "585858", + "606060", + "666666", + "767676", + "808080", + "8a8a8a", + "949494", + "9e9e9e", + "a8a8a8", + "b2b2b2", + "bcbcbc", + "c6c6c6", + "d0d0d0", + "dadada", + "e4e4e4", + "eeeeee" +] diff --git a/app/docker/helpers/logHelper/concatLogsToString.ts b/app/docker/helpers/logHelper/concatLogsToString.ts new file mode 100644 index 000000000..b67d4312e --- /dev/null +++ b/app/docker/helpers/logHelper/concatLogsToString.ts @@ -0,0 +1,15 @@ +import { NEW_LINE_BREAKER } from '@/constants'; + +import { FormattedLine } from './types'; + +type FormatFunc = (line: FormattedLine) => string; + +export function concatLogsToString( + logs: FormattedLine[], + formatFunc: FormatFunc = (line) => line.line +) { + return logs.reduce( + (acc, formattedLine) => acc + formatFunc(formattedLine) + NEW_LINE_BREAKER, + '' + ); +} diff --git a/app/docker/helpers/logHelper/formatJSONLogs.ts b/app/docker/helpers/logHelper/formatJSONLogs.ts new file mode 100644 index 000000000..5a08cc360 --- /dev/null +++ b/app/docker/helpers/logHelper/formatJSONLogs.ts @@ -0,0 +1,55 @@ +import { without } from 'lodash'; + +import { FormattedLine, Span, JSONLogs, TIMESTAMP_LENGTH } from './types'; +import { + formatCaller, + formatKeyValuePair, + formatLevel, + formatMessage, + formatStackTrace, + formatTime, +} from './formatters'; + +function removeKnownKeys(keys: string[]) { + return without(keys, 'time', 'level', 'caller', 'message', 'stack_trace'); +} + +export function formatJSONLine( + rawText: string, + withTimestamps?: boolean +): FormattedLine[] { + const spans: Span[] = []; + const lines: FormattedLine[] = []; + let line = ''; + + const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText; + + const json: JSONLogs = JSON.parse(text); + const { time, level, caller, message, stack_trace: stackTrace } = json; + const keys = removeKnownKeys(Object.keys(json)); + + if (withTimestamps) { + const timestamp = rawText.substring(0, TIMESTAMP_LENGTH); + spans.push({ text: timestamp }); + line += `${timestamp}`; + } + line += formatTime(time, spans, line); + line += formatLevel(level, spans, line); + line += formatCaller(caller, spans, line); + line += formatMessage(message, spans, line, !!keys.length); + + keys.forEach((key, idx) => { + line += formatKeyValuePair( + key, + json[key], + spans, + line, + idx === keys.length - 1 + ); + }); + + lines.push({ line, spans }); + formatStackTrace(stackTrace, lines); + + return lines; +} diff --git a/app/docker/helpers/logHelper/formatLogs.ts b/app/docker/helpers/logHelper/formatLogs.ts new file mode 100644 index 000000000..e0af64375 --- /dev/null +++ b/app/docker/helpers/logHelper/formatLogs.ts @@ -0,0 +1,141 @@ +import tokenize from '@nxmix/tokenize-ansi'; +import { FontWeight } from 'xterm'; + +import { + colors, + BACKGROUND_COLORS_BY_ANSI, + FOREGROUND_COLORS_BY_ANSI, + RGBColor, +} from './colors'; +import { formatJSONLine } from './formatJSONLogs'; +import { formatZerologLogs, ZerologRegex } from './formatZerologLogs'; +import { Token, Span, TIMESTAMP_LENGTH, FormattedLine } from './types'; + +type FormatOptions = { + stripHeaders?: boolean; + withTimestamps?: boolean; + splitter?: string; +}; + +const defaultOptions: FormatOptions = { + splitter: '\n', +}; + +export function formatLogs( + rawLogs: string, + { + stripHeaders, + withTimestamps, + splitter = '\n', + }: FormatOptions = defaultOptions +) { + let logs = rawLogs; + if (stripHeaders) { + logs = stripHeadersFunc(logs); + } + if (logs.includes('\\n')) { + logs = JSON.parse(logs); + } + + const tokens: Token[][] = tokenize(logs); + const formattedLogs: FormattedLine[] = []; + + let fgColor: string | undefined; + let bgColor: string | undefined; + let fontWeight: FontWeight | undefined; + let line = ''; + let spans: Span[] = []; + + tokens.forEach((token) => { + const [type] = token; + + const fgAnsi = FOREGROUND_COLORS_BY_ANSI[type]; + const bgAnsi = BACKGROUND_COLORS_BY_ANSI[type]; + + if (fgAnsi) { + fgColor = cssColorFromRgb(fgAnsi); + } else if (type === 'moreColor') { + fgColor = extendedColorForToken(token); + } else if (type === 'fgDefault') { + fgColor = undefined; + } else if (bgAnsi) { + bgColor = cssColorFromRgb(bgAnsi); + } else if (type === 'bgMoreColor') { + bgColor = extendedColorForToken(token); + } else if (type === 'bgDefault') { + bgColor = undefined; + } else if (type === 'reset') { + fgColor = undefined; + bgColor = undefined; + fontWeight = undefined; + } else if (type === 'bold') { + fontWeight = 'bold'; + } else if (type === 'normal') { + fontWeight = 'normal'; + } else if (type === 'text') { + const tokenLines = (token[1] as string).split(splitter); + + tokenLines.forEach((tokenLine, idx) => { + if (idx && line) { + formattedLogs.push({ line, spans }); + line = ''; + spans = []; + } + + const text = stripEscapeCodes(tokenLine); + if ( + (!withTimestamps && text.startsWith('{')) || + (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{')) + ) { + const lines = formatJSONLine(text, withTimestamps); + formattedLogs.push(...lines); + } else if (ZerologRegex.test(text)) { + const lines = formatZerologLogs(text, withTimestamps); + formattedLogs.push(...lines); + } else { + spans.push({ fgColor, bgColor, text, fontWeight }); + line += text; + } + }); + } + }); + if (line) { + formattedLogs.push({ line, spans }); + } + + return formattedLogs; +} + +function stripHeadersFunc(logs: string) { + return logs.substring(8).replace(/\r?\n(.{8})/g, '\n'); +} + +function stripEscapeCodes(logs: string) { + return logs.replace( + // eslint-disable-next-line no-control-regex + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + '' + ); +} + +function cssColorFromRgb(rgb: RGBColor) { + const [r, g, b] = rgb; + + return `rgb(${r}, ${g}, ${b})`; +} + +// assuming types based on original JS implementation +// there is not much type definitions for the tokenize library +function extendedColorForToken(token: Token[]) { + const [, colorMode, colorRef] = token as [undefined, number, number]; + + if (colorMode === 2) { + return cssColorFromRgb(token.slice(2) as RGBColor); + } + + if (colorMode === 5 && colors[colorRef]) { + return cssColorFromRgb(colors[colorRef]); + } + + return ''; +} diff --git a/app/docker/helpers/logHelper/formatZerologLogs.ts b/app/docker/helpers/logHelper/formatZerologLogs.ts new file mode 100644 index 000000000..e983e3141 --- /dev/null +++ b/app/docker/helpers/logHelper/formatZerologLogs.ts @@ -0,0 +1,119 @@ +import { + formatCaller, + formatKeyValuePair, + formatLevel, + formatMessage, + formatStackTrace, + formatTime, +} from './formatters'; +import { + FormattedLine, + JSONStackTrace, + Level, + Span, + TIMESTAMP_LENGTH, +} from './types'; + +const dateRegex = /(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}[AP]M) /; // "2022/02/01 04:30AM " +const levelRegex = /(\w{3}) /; // "INF " or "ERR " +const callerRegex = /(.+?.go:\d+) /; // "path/to/file.go:line " +const chevRegex = /> /; // "> " +const messageAndPairsRegex = /(.*)/; // include the rest of the string in a separate group + +const keyRegex = /(\S+=)/g; // "" + +export const ZerologRegex = concatRegex( + dateRegex, + levelRegex, + callerRegex, + chevRegex, + messageAndPairsRegex +); + +function concatRegex(...regs: RegExp[]) { + const flags = Array.from( + new Set( + regs + .map((r) => r.flags) + .join('') + .split('') + ) + ).join(''); + const source = regs.map((r) => r.source).join(''); + return new RegExp(source, flags); +} + +type Pair = { + key: string; + value: string; +}; + +export function formatZerologLogs(rawText: string, withTimestamps?: boolean) { + const spans: Span[] = []; + const lines: FormattedLine[] = []; + let line = ''; + + const text = withTimestamps ? rawText.substring(TIMESTAMP_LENGTH) : rawText; + + const [, date, level, caller, messageAndPairs] = + text.match(ZerologRegex) || []; + + const [message, pairs] = extractPairs(messageAndPairs); + + line += formatTime(date, spans, line); + line += formatLevel(level as Level, spans, line); + line += formatCaller(caller, spans, line); + line += formatMessage(message, spans, line, !!pairs.length); + + let stackTrace: JSONStackTrace | undefined; + const stackTraceIndex = pairs.findIndex((p) => p.key === 'stack_trace'); + + if (stackTraceIndex !== -1) { + stackTrace = JSON.parse(pairs[stackTraceIndex].value); + pairs.splice(stackTraceIndex); + } + + pairs.forEach(({ key, value }, idx) => { + line += formatKeyValuePair( + key, + value, + spans, + line, + idx === pairs.length - 1 + ); + }); + lines.push({ line, spans }); + + formatStackTrace(stackTrace, lines); + + return lines; +} + +function extractPairs(messageAndPairs: string): [string, Pair[]] { + const pairs: Pair[] = []; + let [message, rawPairs] = messageAndPairs.split('|'); + + if (!messageAndPairs.includes('|') && !rawPairs) { + rawPairs = message; + message = ''; + } + message = message.trim(); + rawPairs = rawPairs.trim(); + + const matches = [...rawPairs.matchAll(keyRegex)]; + + matches.forEach((m, idx) => { + const rawKey = m[0]; + const key = rawKey.slice(0, -1); + const start = m.index || 0; + const end = idx !== matches.length - 1 ? matches[idx + 1].index : undefined; + const value = ( + end + ? rawPairs.slice(start + rawKey.length, end) + : rawPairs.slice(start + rawKey.length) + ).trim(); + pairs.push({ key, value }); + }); + + return [message, pairs]; +} diff --git a/app/docker/helpers/logHelper/formatters.ts b/app/docker/helpers/logHelper/formatters.ts new file mode 100644 index 000000000..10c5b284f --- /dev/null +++ b/app/docker/helpers/logHelper/formatters.ts @@ -0,0 +1,154 @@ +import { format } from 'date-fns'; +import { takeRight } from 'lodash'; + +import { Span, Level, Colors, JSONStackTrace, FormattedLine } from './types'; + +const spaceSpan: Span = { text: ' ' }; + +function logLevelToSpan(level: Level): Span { + switch (level) { + case 'debug': + case 'DBG': + return { + fgColor: Colors.Grey, + text: 'DBG', + fontWeight: 'bold', + }; + case 'info': + case 'INF': + return { + fgColor: Colors.Green, + text: 'INF', + fontWeight: 'bold', + }; + case 'warn': + case 'WRN': + return { + fgColor: Colors.Yellow, + text: 'WRN', + fontWeight: 'bold', + }; + case 'error': + case 'ERR': + return { + fgColor: Colors.Red, + text: 'ERR', + fontWeight: 'bold', + }; + default: + return { text: level }; + } +} + +export function formatTime( + time: number | string | undefined, + spans: Span[], + line: string +) { + let nl = line; + if (time) { + let date = ''; + if (typeof time === 'number') { + date = format(new Date(time * 1000), 'Y/MM/dd hh:mmaa'); + } else { + date = time; + } + spans.push({ fgColor: Colors.Grey, text: date }, spaceSpan); + nl += `${date} `; + } + return nl; +} + +export function formatLevel( + level: Level | undefined, + spans: Span[], + line: string +) { + let nl = line; + if (level) { + const levelSpan = logLevelToSpan(level); + spans.push(levelSpan, spaceSpan); + nl += `${levelSpan.text} `; + } + return nl; +} + +export function formatCaller( + caller: string | undefined, + spans: Span[], + line: string +) { + let nl = line; + if (caller) { + const trim = takeRight(caller.split('/'), 2).join('/'); + spans.push( + { fgColor: Colors.Magenta, text: trim, fontWeight: 'bold' }, + spaceSpan + ); + spans.push({ fgColor: Colors.Blue, text: '>' }, spaceSpan); + nl += `${trim} > `; + } + return nl; +} + +export function formatMessage( + message: string, + spans: Span[], + line: string, + hasKeys: boolean +) { + let nl = line; + if (message) { + spans.push({ fgColor: Colors.Magenta, text: `${message}` }, spaceSpan); + nl += `${message} `; + + if (hasKeys) { + spans.push({ fgColor: Colors.Magenta, text: `|` }, spaceSpan); + nl += '| '; + } + } + return nl; +} + +export function formatKeyValuePair( + key: string, + value: unknown, + spans: Span[], + line: string, + isLastKey: boolean +) { + let nl = line; + + spans.push( + { fgColor: Colors.Blue, text: `${key}=` }, + { + fgColor: key === 'error' || key === 'ERR' ? Colors.Red : Colors.Magenta, + text: value as string, + } + ); + if (!isLastKey) spans.push(spaceSpan); + nl += `${key}=${value}${!isLastKey ? ' ' : ''}`; + + return nl; +} + +export function formatStackTrace( + stackTrace: JSONStackTrace | undefined, + lines: FormattedLine[] +) { + if (stackTrace) { + stackTrace.forEach(({ func, line: lineNumber, source }) => { + const line = ` at ${func} (${source}:${lineNumber})`; + const spans: Span[] = [ + spaceSpan, + spaceSpan, + spaceSpan, + spaceSpan, + { text: 'at ', fgColor: Colors.Grey }, + { text: func, fgColor: Colors.Red }, + { text: `(${source}:${lineNumber})`, fgColor: Colors.Grey }, + ]; + lines.push({ line, spans }); + }); + } +} diff --git a/app/docker/helpers/logHelper/index.ts b/app/docker/helpers/logHelper/index.ts new file mode 100644 index 000000000..91fe97ed7 --- /dev/null +++ b/app/docker/helpers/logHelper/index.ts @@ -0,0 +1,2 @@ +export { formatLogs } from './formatLogs'; +export { concatLogsToString } from './concatLogsToString'; diff --git a/app/docker/helpers/logHelper/types.ts b/app/docker/helpers/logHelper/types.ts new file mode 100644 index 000000000..7d467e296 --- /dev/null +++ b/app/docker/helpers/logHelper/types.ts @@ -0,0 +1,53 @@ +import { FontWeight } from 'xterm'; + +import { type TextColor } from './colors'; + +export type Token = string | number; + +export type Level = + | 'debug' + | 'info' + | 'warn' + | 'error' + | 'DBG' + | 'INF' + | 'WRN' + | 'ERR'; + +export type JSONStackTrace = { + func: string; + line: string; + source: string; +}[]; + +export type JSONLogs = { + [k: string]: unknown; + time: number; + level: Level; + caller: string; + message: string; + stack_trace?: JSONStackTrace; +}; + +export type Span = { + fgColor?: TextColor; + bgColor?: TextColor; + text: string; + fontWeight?: FontWeight; +}; + +export type FormattedLine = { + spans: Span[]; + line: string; +}; + +export const TIMESTAMP_LENGTH = 31; // 30 for timestamp + 1 for trailing space + +export const Colors = { + Grey: 'var(--text-log-viewer-color-json-grey)', + Magenta: 'var(--text-log-viewer-color-json-magenta)', + Yellow: 'var(--text-log-viewer-color-json-yellow)', + Green: 'var(--text-log-viewer-color-json-green)', + Red: 'var(--text-log-viewer-color-json-red)', + Blue: 'var(--text-log-viewer-color-json-blue)', +}; diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index fc1514b6f..175720b09 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -10,11 +10,12 @@ import { stopContainer, } from '@/react/docker/containers/containers.service'; import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container'; +import { formatLogs } from '../helpers/logHelper'; angular.module('portainer.docker').factory('ContainerService', ContainerServiceFactory); /* @ngInject */ -function ContainerServiceFactory($q, Container, LogHelper, $timeout, EndpointProvider) { +function ContainerServiceFactory($q, Container, $timeout, EndpointProvider) { const service = { killContainer: withEndpointId(killContainer), pauseContainer: withEndpointId(pauseContainer), @@ -159,7 +160,7 @@ function ContainerServiceFactory($q, Container, LogHelper, $timeout, EndpointPro Container.logs(parameters) .$promise.then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps }); + var logs = formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps }); deferred.resolve(logs); }) .catch(function error(err) { diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js index cab03024b..749bc91c8 100644 --- a/app/docker/services/serviceService.js +++ b/app/docker/services/serviceService.js @@ -1,13 +1,10 @@ +import { formatLogs } from '../helpers/logHelper'; import { ServiceViewModel } from '../models/service'; angular.module('portainer.docker').factory('ServiceService', [ '$q', 'Service', - 'ServiceHelper', - 'TaskService', - 'ResourceControlService', - 'LogHelper', - function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, ResourceControlService, LogHelper) { + function ServiceServiceFactory($q, Service) { 'use strict'; var service = {}; @@ -88,7 +85,7 @@ angular.module('portainer.docker').factory('ServiceService', [ Service.logs(parameters) .$promise.then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps }); + var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps }); deferred.resolve(logs); }) .catch(function error(err) { diff --git a/app/docker/services/system.service.ts b/app/docker/services/system.service.ts index dae7dd402..b7a16aaf9 100644 --- a/app/docker/services/system.service.ts +++ b/app/docker/services/system.service.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; export interface VersionResponse { diff --git a/app/docker/services/taskService.js b/app/docker/services/taskService.js index 5c38a316f..0f68183a0 100644 --- a/app/docker/services/taskService.js +++ b/app/docker/services/taskService.js @@ -1,10 +1,10 @@ +import { formatLogs } from '../helpers/logHelper'; import { TaskViewModel } from '../models/task'; angular.module('portainer.docker').factory('TaskService', [ '$q', 'Task', - 'LogHelper', - function TaskServiceFactory($q, Task, LogHelper) { + function TaskServiceFactory($q, Task) { 'use strict'; var service = {}; @@ -54,7 +54,7 @@ angular.module('portainer.docker').factory('TaskService', [ Task.logs(parameters) .$promise.then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps }); + var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps }); deferred.resolve(logs); }) .catch(function error(err) { diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js index 013dbf9f7..4870625e7 100644 --- a/app/docker/views/configs/configsController.js +++ b/app/docker/views/configs/configsController.js @@ -1,4 +1,5 @@ import angular from 'angular'; +import { confirmDeletionAsync } from 'Portainer/services/modal.service/confirm'; class ConfigsController { /* @ngInject */ @@ -31,7 +32,11 @@ class ConfigsController { this.getConfigs(); } - removeAction(selectedItems) { + async removeAction(selectedItems) { + const confirmed = await confirmDeletionAsync('Do you want to remove the selected config(s)?'); + if (!confirmed) { + return null; + } return this.$async(this.removeActionAsync, selectedItems); } diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index 6289f8fb3..43031bc89 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,6 +1,7 @@ import _ from 'lodash-es'; import DockerNetworkHelper from 'Docker/helpers/networkHelper'; import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper'; +import { confirmDeletionAsync } from 'Portainer/services/modal.service/confirm'; angular.module('portainer.docker').controller('NetworksController', [ '$q', @@ -12,7 +13,11 @@ angular.module('portainer.docker').controller('NetworksController', [ 'endpoint', 'AgentService', function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) { - $scope.removeAction = function (selectedItems) { + $scope.removeAction = async function (selectedItems) { + const confirmed = await confirmDeletionAsync('Do you want to remove the selected network(s)?'); + if (!confirmed) { + return null; + } var actionCount = selectedItems.length; angular.forEach(selectedItems, function (network) { HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); diff --git a/app/docker/views/secrets/secretsController.js b/app/docker/views/secrets/secretsController.js index 69d22b196..2b14e0e1a 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -1,10 +1,15 @@ +import { confirmDeletionAsync } from 'Portainer/services/modal.service/confirm'; angular.module('portainer.docker').controller('SecretsController', [ '$scope', '$state', 'SecretService', 'Notifications', function ($scope, $state, SecretService, Notifications) { - $scope.removeAction = function (selectedItems) { + $scope.removeAction = async function (selectedItems) { + const confirmed = await confirmDeletionAsync('Do you want to remove the selected secret(s)?'); + if (!confirmed) { + return null; + } var actionCount = selectedItems.length; angular.forEach(selectedItems, function (secret) { SecretService.remove(secret.Id) diff --git a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx index b6bb34561..1dda53530 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx @@ -3,7 +3,7 @@ import { usePagination, useTable } from 'react-table'; import { Device } from '@/portainer/hostmanagement/open-amt/model'; import { useAMTDevices } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices'; import { RowProvider } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import PortainerError from '@/portainer/error'; import { InnerDatatable } from '@@/datatables/InnerDatatable'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx index 1d77071ac..461fa063c 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useMemo, useReducer } from 'react'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { createRowContext } from '@@/datatables/RowContext'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx index a38ad5816..8523c0e1f 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'; import { useQuery } from 'react-query'; import { getDevices } from '@/portainer/hostmanagement/open-amt/open-amt.service'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import PortainerError from '@/portainer/error'; import * as notifications from '@/portainer/services/notifications'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx index 8c8384cda..f46205f2d 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx @@ -2,7 +2,7 @@ import { useTable, useExpanded, useSortBy, useFilters } from 'react-table'; import { useRowSelectColumn } from '@lineup-lite/hooks'; import _ from 'lodash'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { AMTDevicesDatatable } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable'; import { EnvironmentGroup } from '@/portainer/environment-groups/types'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx index 9e052651b..c8768769a 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx @@ -1,6 +1,6 @@ import { useRouter } from '@uirouter/react'; -import type { Environment } from '@/portainer/environments/types'; +import type { Environment } from '@/react/portainer/environments/types'; import { confirmAsync, confirmDestructiveAsync, @@ -8,7 +8,7 @@ import { import { promptAsync } from '@/portainer/services/modal.service/prompt'; import * as notifications from '@/portainer/services/notifications'; import { activateDevice } from '@/portainer/hostmanagement/open-amt/open-amt.service'; -import { deleteEndpoint } from '@/portainer/environments/environment.service'; +import { deleteEndpoint } from '@/react/portainer/environments/environment.service'; import { Button } from '@@/buttons'; import { Link } from '@@/Link'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx index c6e2b6672..49a81337d 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList'; -import { EdgeTypes, Environment } from '@/portainer/environments/types'; +import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList'; +import { EdgeTypes, Environment } from '@/react/portainer/environments/types'; import { useDebounce } from '@/portainer/hooks/useDebounce'; import { useSearchBarState } from '@@/datatables/SearchBar'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx index 19c282c1b..1fd236324 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx @@ -2,10 +2,10 @@ import { CellProps, Column } from 'react-table'; import { MenuItem, MenuLink } from '@reach/menu-button'; import { useRouter, useSref } from '@uirouter/react'; -import { Environment } from '@/portainer/environments/types'; -import { snapshotEndpoint } from '@/portainer/environments/environment.service'; +import { Environment } from '@/react/portainer/environments/types'; +import { snapshotEndpoint } from '@/react/portainer/environments/environment.service'; import * as notifications from '@/portainer/services/notifications'; -import { getRoute } from '@/portainer/environments/utils'; +import { getRoute } from '@/react/portainer/environments/utils'; import { ActionsMenu } from '@@/datatables/ActionsMenu'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx index 69d8dcf38..45e8a52eb 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx @@ -1,6 +1,6 @@ import { Column } from 'react-table'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { DefaultFilter } from '@@/datatables/Filter'; diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx index ba8bfc3fa..b3adba5ec 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx @@ -1,6 +1,6 @@ import { CellProps, Column } from 'react-table'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { EdgeIndicator } from '@/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator'; export const heartbeat: Column = { diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx index 65db6a6ef..197a2ba9e 100644 --- a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx @@ -1,6 +1,6 @@ import { CellProps, Column } from 'react-table'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { Link } from '@@/Link'; import { ExpandingCell } from '@@/datatables/ExpandingCell'; diff --git a/app/edge/EdgeDevices/WaitingRoomView/Datatable/Datatable.tsx b/app/edge/EdgeDevices/WaitingRoomView/Datatable/Datatable.tsx index 1dea9fd06..a63870258 100644 --- a/app/edge/EdgeDevices/WaitingRoomView/Datatable/Datatable.tsx +++ b/app/edge/EdgeDevices/WaitingRoomView/Datatable/Datatable.tsx @@ -8,7 +8,7 @@ import { } from 'react-table'; import { useRowSelectColumn } from '@lineup-lite/hooks'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { notifySuccess } from '@/portainer/services/notifications'; import { Button } from '@@/buttons'; diff --git a/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx b/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx index 698181b84..627021e7c 100644 --- a/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx +++ b/app/edge/EdgeDevices/WaitingRoomView/WaitingRoomView.tsx @@ -1,7 +1,7 @@ import { useRouter } from '@uirouter/react'; -import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList'; -import { EdgeTypes } from '@/portainer/environments/types'; +import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList'; +import { EdgeTypes } from '@/react/portainer/environments/types'; import { InformationPanel } from '@@/InformationPanel'; import { TextTip } from '@@/Tip/TextTip'; diff --git a/app/edge/EdgeDevices/WaitingRoomView/queries.ts b/app/edge/EdgeDevices/WaitingRoomView/queries.ts index a7dd8e5e0..a930de9af 100644 --- a/app/edge/EdgeDevices/WaitingRoomView/queries.ts +++ b/app/edge/EdgeDevices/WaitingRoomView/queries.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { promiseSequence } from '@/portainer/helpers/promise-utils'; diff --git a/app/edge/components/group-form/groupFormController.js b/app/edge/components/group-form/groupFormController.js index 7ff76bf18..1bed6652d 100644 --- a/app/edge/components/group-form/groupFormController.js +++ b/app/edge/components/group-form/groupFormController.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { confirmDestructiveAsync } from '@/portainer/services/modal.service/confirm'; -import { EdgeTypes } from '@/portainer/environments/types'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { EdgeTypes } from '@/react/portainer/environments/types'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; export class EdgeGroupFormController { /* @ngInject */ diff --git a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js index 1251e41ad..dc004461f 100644 --- a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js +++ b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; export class EdgeJobController { /* @ngInject */ diff --git a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js index 0b5bef7da..9110610c5 100644 --- a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; export class EditEdgeStackViewController { /* @ngInject */ diff --git a/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.html b/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.html index 423a3f1a9..0e7a4a7fb 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.html +++ b/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.html @@ -2,22 +2,9 @@
- - + + - -
- -
- -
diff --git a/app/kubernetes/components/helm/helm-templates/helm-templates.html b/app/kubernetes/components/helm/helm-templates/helm-templates.html index aa46a63bf..22deaf322 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-templates.html +++ b/app/kubernetes/components/helm/helm-templates/helm-templates.html @@ -24,13 +24,7 @@
- + {{ $ctrl.state.chart.name }}
diff --git a/app/kubernetes/pod/service.js b/app/kubernetes/pod/service.js index 925b09797..45d735700 100644 --- a/app/kubernetes/pod/service.js +++ b/app/kubernetes/pod/service.js @@ -67,7 +67,7 @@ class KubernetesPodService { params.container = containerName; } const data = await this.KubernetesPods(namespace).logs(params).$promise; - return data.logs.length === 0 ? [] : data.logs.split('\n'); + return data.logs; } catch (err) { throw new PortainerError('Unable to retrieve pod logs', err); } diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index bf0613ea2..b4a59cfb4 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -12,9 +12,10 @@ export const componentsModule = angular .component( 'ingressClassDatatable', r2a(IngressClassDatatable, [ - 'onChangeAvailability', + 'onChangeControllers', 'description', 'ingressControllers', + 'allowNoneIngressClass', 'isLoading', 'noIngressControllerLabel', 'view', diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx index 7a1ad6ba0..23ee4aca7 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx @@ -157,7 +157,9 @@ export function CreateIngressView() { const existingIngressClass = useMemo( () => ingressControllersResults.data?.find( - (i) => i.ClassName === ingressRule.IngressClassName + (i) => + i.ClassName === ingressRule.IngressClassName || + (i.Type === 'custom' && ingressRule.IngressClassName === '') ), [ingressControllersResults.data, ingressRule.IngressClassName] ); @@ -175,12 +177,13 @@ export function CreateIngressView() { (!existingIngressClass || (existingIngressClass && !existingIngressClass.Availability)) && ingressRule.IngressClassName && - ingressControllersResults.data + !ingressControllersResults.isLoading ) { + const optionLabel = !ingressRule.IngressType + ? `${ingressRule.IngressClassName} - NOT FOUND` + : `${ingressRule.IngressClassName} - DISALLOWED`; ingressClassOptions.push({ - label: !ingressRule.IngressType - ? `${ingressRule.IngressClassName} - NOT FOUND` - : `${ingressRule.IngressClassName} - DISALLOWED`, + label: optionLabel, value: ingressRule.IngressClassName, }); } @@ -206,7 +209,8 @@ export function CreateIngressView() { !!params.name && ingressesResults.data && !ingressRule.IngressName && - ingressControllersResults.data + !ingressControllersResults.isLoading && + !ingressControllersResults.isLoading ) { // if it is an edit screen, prepare the rule from the ingress const ing = ingressesResults.data?.find( @@ -214,13 +218,16 @@ export function CreateIngressView() { ); if (ing) { const type = ingressControllersResults.data?.find( - (c) => c.ClassName === ing.ClassName + (c) => + c.ClassName === ing.ClassName || + (c.Type === 'custom' && !ing.ClassName) )?.Type; - const r = prepareRuleFromIngress(ing); + const r = prepareRuleFromIngress(ing, type); r.IngressType = type || r.IngressType; setIngressRule(r); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ params.name, ingressesResults.data, @@ -635,7 +642,7 @@ export function CreateIngressView() { setIngressRule(rule); } - function addNewAnnotation(type?: 'rewrite' | 'regex') { + function addNewAnnotation(type?: 'rewrite' | 'regex' | 'ingressClass') { const rule = { ...ingressRule }; const annotation: Annotation = { @@ -643,13 +650,21 @@ export function CreateIngressView() { Value: '', ID: uuidv4(), }; - if (type === 'rewrite') { - annotation.Key = 'nginx.ingress.kubernetes.io/rewrite-target'; - annotation.Value = '/$1'; - } - if (type === 'regex') { - annotation.Key = 'nginx.ingress.kubernetes.io/use-regex'; - annotation.Value = 'true'; + switch (type) { + case 'rewrite': + annotation.Key = 'nginx.ingress.kubernetes.io/rewrite-target'; + annotation.Value = '/$1'; + break; + case 'regex': + annotation.Key = 'nginx.ingress.kubernetes.io/use-regex'; + annotation.Value = 'true'; + break; + case 'ingressClass': + annotation.Key = 'kubernetes.io/ingress.class'; + annotation.Value = ''; + break; + default: + break; } rule.Annotations = rule.Annotations || []; rule.Annotations?.push(annotation); @@ -689,10 +704,13 @@ export function CreateIngressView() { function handleCreateIngressRules() { const rule = { ...ingressRule }; + const classNameToSend = + rule.IngressClassName === 'none' ? '' : rule.IngressClassName; + const ingress: Ingress = { Namespace: namespace, Name: rule.IngressName, - ClassName: rule.IngressClassName, + ClassName: classNameToSend, Hosts: rule.Hosts.map((host) => host.Host), Paths: preparePaths(rule.IngressName, rule.Hosts), TLS: prepareTLS(rule.Hosts), diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx index 7e22d7c61..87e49c645 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx @@ -47,7 +47,7 @@ interface Props { addNewIngressHost: (noHost?: boolean) => void; addNewIngressRoute: (hostIndex: number) => void; - addNewAnnotation: (type?: 'rewrite' | 'regex') => void; + addNewAnnotation: (type?: 'rewrite' | 'regex' | 'ingressClass') => void; handleNamespaceChange: (val: string) => void; handleHostChange: (hostIndex: number, val: string) => void; @@ -249,9 +249,10 @@ export function IngressForm({ onClick={() => addNewAnnotation('rewrite')} icon={Plus} title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to." + data-cy="add-rewrite-annotation" > {' '} - add rewrite annotation + Add rewrite annotation )} + + {rule.IngressType === 'custom' && ( + + )}
Rules
diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts index 53adeaa20..79204e78e 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { Annotation } from '@/kubernetes/react/views/networks/ingresses/components/annotations/types'; +import { SupportedIngControllerTypes } from '@/react/kubernetes/cluster/ingressClass/types'; import { TLS, Ingress } from '../types'; @@ -62,7 +63,7 @@ export function prepareRuleHostsFromIngress(ing: Ingress) { h.Host = host; h.Secret = getSecretByHost(host, ing.TLS); h.Paths = []; - ing.Paths.forEach((path) => { + ing.Paths?.forEach((path) => { if (path.Host === host) { h.Paths.push({ Route: path.Path, @@ -99,12 +100,15 @@ export function getAnnotationsForEdit( return result; } -export function prepareRuleFromIngress(ing: Ingress): Rule { +export function prepareRuleFromIngress( + ing: Ingress, + type?: SupportedIngControllerTypes +): Rule { return { Key: uuidv4(), IngressName: ing.Name, Namespace: ing.Namespace, - IngressClassName: ing.ClassName, + IngressClassName: type === 'custom' ? 'none' : ing.ClassName, Hosts: prepareRuleHostsFromIngress(ing) || [], Annotations: ing.Annotations ? getAnnotationsForEdit(ing.Annotations) : [], IngressType: ing.Type, diff --git a/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx b/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx index 738c432ef..f4f201873 100644 --- a/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx +++ b/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx @@ -3,7 +3,7 @@ import { useRouter } from '@uirouter/react'; import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId'; import { useNamespaces } from '@/react/kubernetes/namespaces/queries'; -import { Authorized } from '@/portainer/hooks/useUser'; +import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser'; import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm'; import { Datatable } from '@@/datatables'; @@ -55,6 +55,7 @@ export function IngressDataTable() { }} getRowId={(row) => row.Name + row.Type + row.Namespace} renderTableActions={tableActions} + disableSelect={useCheckboxes()} /> ); @@ -80,7 +81,7 @@ export function IngressDataTable() { - +
diff --git a/app/kubernetes/views/applications/logs/logsController.js b/app/kubernetes/views/applications/logs/logsController.js index a72e6acb1..66601d98b 100644 --- a/app/kubernetes/views/applications/logs/logsController.js +++ b/app/kubernetes/views/applications/logs/logsController.js @@ -1,5 +1,6 @@ import angular from 'angular'; -import _ from 'lodash-es'; + +import { concatLogsToString, formatLogs } from '@/docker/helpers/logHelper'; class KubernetesApplicationLogsController { /* @ngInject */ @@ -39,13 +40,15 @@ class KubernetesApplicationLogsController { } downloadLogs() { - const data = new this.Blob([_.reduce(this.applicationLogs, (acc, log) => acc + '\n' + log, '')]); + const logsAsString = concatLogsToString(this.applicationLogs); + const data = new this.Blob([logsAsString]); this.FileSaver.saveAs(data, this.podName + '_logs.txt'); } async getApplicationLogsAsync() { try { - this.applicationLogs = await this.KubernetesPodService.logs(this.application.ResourcePool, this.podName, this.containerName); + const rawLogs = await this.KubernetesPodService.logs(this.application.ResourcePool, this.podName, this.containerName); + this.applicationLogs = formatLogs(rawLogs); } catch (err) { this.stopRepeater(); this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); @@ -70,13 +73,8 @@ class KubernetesApplicationLogsController { this.containerName = containerName; try { - const [application, applicationLogs] = await Promise.all([ - this.KubernetesApplicationService.get(namespace, applicationName), - this.KubernetesPodService.logs(namespace, podName, containerName), - ]); - - this.application = application; - this.applicationLogs = applicationLogs; + this.application = await this.KubernetesApplicationService.get(namespace, applicationName); + await this.getApplicationLogsAsync(); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); } finally { diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 879a034cf..29f9bdad3 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -42,7 +42,8 @@
-
-
- - + +
+
+
+ + +
+
+
+
+ + +
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index eb655517b..080c9a6e0 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -47,9 +47,10 @@ class KubernetesConfigureController { this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT; this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW; this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this); - this.onChangeAvailability = this.onChangeAvailability.bind(this); + this.onChangeControllers = this.onChangeControllers.bind(this); this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this); this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this); + this.onToggleAllowNoneIngressClass = this.onToggleAllowNoneIngressClass.bind(this); this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this); } /* #endregion */ @@ -71,7 +72,7 @@ class KubernetesConfigureController { /* #endregion */ /* #region INGRESS CLASSES UI MANAGEMENT */ - onChangeAvailability(controllerClassMap) { + onChangeControllers(controllerClassMap) { this.ingressControllers = controllerClassMap; } @@ -79,6 +80,18 @@ class KubernetesConfigureController { return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK }); } + toggleAdvancedIngSettings() { + this.$scope.$evalAsync(() => { + this.state.isIngToggleSectionExpanded = !this.state.isIngToggleSectionExpanded; + }); + } + + onToggleAllowNoneIngressClass() { + this.$scope.$evalAsync(() => { + this.formValues.AllowNoneIngressClass = !this.formValues.AllowNoneIngressClass; + }); + } + onToggleIngressAvailabilityPerNamespace() { this.$scope.$evalAsync(() => { this.formValues.IngressAvailabilityPerNamespace = !this.formValues.IngressAvailabilityPerNamespace; @@ -109,6 +122,7 @@ class KubernetesConfigureController { endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses; endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace; endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = this.formValues.IngressAvailabilityPerNamespace; + endpoint.Kubernetes.Configuration.AllowNoneIngressClass = this.formValues.AllowNoneIngressClass; endpoint.ChangeWindow = this.state.autoUpdateSettings; } @@ -256,6 +270,7 @@ class KubernetesConfigureController { actionInProgress: false, displayConfigureClassPanel: {}, viewReady: false, + isIngToggleSectionExpanded: false, endpointId: this.$state.params.endpointId, duplicates: { ingressClasses: new KubernetesFormValidationReferences(), @@ -315,6 +330,7 @@ class KubernetesConfigureController { return ic; }); this.formValues.IngressAvailabilityPerNamespace = this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace; + this.formValues.AllowNoneIngressClass = this.endpoint.Kubernetes.Configuration.AllowNoneIngressClass; this.oldFormValues = Object.assign({}, this.formValues); } catch (err) { diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 56a88b40a..dbe1365e9 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -185,7 +185,7 @@
Networking
Networking
-

{{ line.AppName }} {{ line.Line }}

No log line matching the '{{ ctrl.state.search }}' filter

No logs available

+
+        

{{ log.appName }} {{ span.text }}

+

No log line matching the '{{ ctrl.state.search }}' filter

+

No logs available

+
diff --git a/app/kubernetes/views/stacks/logs/logsController.js b/app/kubernetes/views/stacks/logs/logsController.js index 547ba9e6c..536ea2ae4 100644 --- a/app/kubernetes/views/stacks/logs/logsController.js +++ b/app/kubernetes/views/stacks/logs/logsController.js @@ -1,6 +1,7 @@ -import _ from 'lodash-es'; +import { filter, flatMap, map } from 'lodash'; import angular from 'angular'; import $allSettled from 'Portainer/services/allSettled'; +import { concatLogsToString, formatLogs } from '@/docker/helpers/logHelper'; const colors = ['red', 'orange', 'lime', 'green', 'darkgreen', 'cyan', 'turquoise', 'teal', 'deepskyblue', 'blue', 'darkblue', 'slateblue', 'magenta', 'darkviolet']; @@ -58,7 +59,7 @@ class KubernetesStackLogsController { Pods: [], }; - const promises = _.flatMap(_.map(app.Pods, (pod) => _.map(pod.Containers, (container) => this.generateLogsPromise(pod, container)))); + const promises = flatMap(map(app.Pods, (pod) => map(pod.Containers, (container) => this.generateLogsPromise(pod, container)))); const result = await $allSettled(promises); res.Pods = result.fulfilled; return res; @@ -67,21 +68,12 @@ class KubernetesStackLogsController { async getStackLogsAsync() { try { const applications = await this.KubernetesApplicationService.get(this.state.transition.namespace); - const filteredApplications = _.filter(applications, (app) => app.StackName === this.state.transition.name); - const logsPromises = _.map(filteredApplications, this.generateAppPromise); + const filteredApplications = filter(applications, (app) => app.StackName === this.state.transition.name); + const logsPromises = map(filteredApplications, this.generateAppPromise); const data = await Promise.all(logsPromises); - const logs = _.flatMap(data, (app, index) => { - return _.flatMap(app.Pods, (pod) => { - return _.map(pod.Logs, (line) => { - const res = { - Color: colors[index % colors.length], - Line: line, - AppName: pod.Pod.Name, - }; - return res; - }); - }); - }); + const logs = flatMap(data, (app, index) => + flatMap(app.Pods, (pod) => formatLogs(pod.Logs).map((line) => ({ ...line, appColor: colors[index % colors.length], appName: pod.Pod.Name }))) + ); this.stackLogs = logs; } catch (err) { this.stopRepeater(); @@ -90,7 +82,8 @@ class KubernetesStackLogsController { } downloadLogs() { - const data = new this.Blob([(this.dataLogs = _.reduce(this.stackLogs, (acc, log) => acc + '\n' + log.AppName + ' ' + log.Line, ''))]); + const logsAsString = concatLogsToString(this.state.filteredLogs, (line) => `${line.appName} ${line.line}`); + const data = new this.Blob([logsAsString]); this.FileSaver.saveAs(data, this.state.transition.name + '_logs.txt'); } diff --git a/app/portainer/components/BEFeatureIndicator/BEFeatureIndicator.html b/app/portainer/components/BEFeatureIndicator/BEFeatureIndicator.html index ce3114a64..e68b4a4f6 100644 --- a/app/portainer/components/BEFeatureIndicator/BEFeatureIndicator.html +++ b/app/portainer/components/BEFeatureIndicator/BEFeatureIndicator.html @@ -1,4 +1,4 @@ - + Business Edition Feature diff --git a/app/portainer/components/accessControlForm/porAccessControlFormController.js b/app/portainer/components/accessControlForm/porAccessControlFormController.js index 86bde68f3..05a6e35dd 100644 --- a/app/portainer/components/accessControlForm/porAccessControlFormController.js +++ b/app/portainer/components/accessControlForm/porAccessControlFormController.js @@ -71,19 +71,16 @@ angular.module('portainer.app').controller('porAccessControlFormController', [ }) .then(function success(data) { ctrl.availableUsers = _.orderBy(data.availableUsers, 'Username', 'asc'); - - var availableTeams = _.orderBy(data.availableTeams, 'Name', 'asc'); - ctrl.availableTeams = availableTeams; - if (!isAdmin && availableTeams.length === 1) { - ctrl.formData.AuthorizedTeams = availableTeams; + ctrl.availableTeams = _.orderBy(data.availableTeams, 'Name', 'asc'); + if (!isAdmin && ctrl.availableTeams.length === 1) { + ctrl.formData.AuthorizedTeams = ctrl.availableTeams; } - return $q.when(ctrl.resourceControl && ResourceControlService.retrieveOwnershipDetails(ctrl.resourceControl)); }) .then(function success(data) { if (data) { - var authorizedUsers = data.authorizedUsers; - var authorizedTeams = data.authorizedTeams; + const authorizedTeams = !isAdmin && ctrl.availableTeams.length === 1 ? ctrl.availableTeams : data.authorizedTeams; + const authorizedUsers = !isAdmin && authorizedTeams.length === 1 ? [] : data.authorizedUsers; setOwnership(ctrl.resourceControl, isAdmin); setAuthorizedUsersAndTeams(authorizedUsers, authorizedTeams); } diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js index 940573b95..8e8a4d9c0 100644 --- a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js @@ -1,8 +1,9 @@ import angular from 'angular'; import _ from 'lodash-es'; -import { EdgeTypes } from '@/portainer/environments/types'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { EdgeTypes } from '@/react/portainer/environments/types'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; +import { getTags } from '@/portainer/tags/tags.service'; class AssoicatedEndpointsSelectorController { /* @ngInject */ @@ -47,6 +48,7 @@ class AssoicatedEndpointsSelectorController { } loadData() { + this.getTags(); this.getAvailableEndpoints(); this.getAssociatedEndpoints(); } @@ -79,6 +81,14 @@ class AssoicatedEndpointsSelectorController { this.setTableData('associated', response.value, response.totalCount); }); } + + getTags() { + return this.$async(async () => { + let tags = { value: [], totalCount: 0 }; + tags = await getTags(); + this.tags = tags; + }); + } /* #endregion */ /* #region On endpoint click (either available or associated) */ diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html index b76479f6a..8683d2780 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html @@ -110,7 +110,7 @@ Manage access - Browse + Browse - diff --git a/app/portainer/components/form-components/web-editor-form/web-editor-form.html b/app/portainer/components/form-components/web-editor-form/web-editor-form.html index f72bfbf41..b644c2964 100644 --- a/app/portainer/components/form-components/web-editor-form/web-editor-form.html +++ b/app/portainer/components/form-components/web-editor-form/web-editor-form.html @@ -1,5 +1,5 @@ -
+
Web editor
diff --git a/app/portainer/components/forms/group-form/groupFormController.js b/app/portainer/components/forms/group-form/groupFormController.js index 2e2ac5524..22dce5741 100644 --- a/app/portainer/components/forms/group-form/groupFormController.js +++ b/app/portainer/components/forms/group-form/groupFormController.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; import angular from 'angular'; -import { endpointsByGroup } from '@/portainer/environments/environment.service'; +import { endpointsByGroup } from '@/react/portainer/environments/environment.service'; import { notifyError } from '@/portainer/services/notifications'; class GroupFormController { diff --git a/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html b/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html index a45f42525..42bd81a06 100644 --- a/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html +++ b/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html @@ -17,19 +17,20 @@
-
-
-
-
-
-

- - This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123'). -

-

This field is required.

+
+
+
+

+ + This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123'). +

+

This field is required.

+
+
+
@@ -66,8 +67,16 @@ Deployment in progress... -
{{ $ctrl.state.formValidationError }}
-
This template type cannot be deployed on this environment.
+
+
+

{{ $ctrl.state.formValidationError }}

+
+
+
+
+

This template type cannot be deployed on this environment.

+
+
diff --git a/app/portainer/components/template-list/template-item/templateItem.html b/app/portainer/components/template-list/template-item/templateItem.html index 3b2090f3f..d2755777c 100644 --- a/app/portainer/components/template-list/template-item/templateItem.html +++ b/app/portainer/components/template-list/template-item/templateItem.html @@ -3,13 +3,7 @@
- +
diff --git a/app/portainer/components/template-list/templateList.html b/app/portainer/components/template-list/templateList.html index fd65eae0e..c4d4b30de 100644 --- a/app/portainer/components/template-list/templateList.html +++ b/app/portainer/components/template-list/templateList.html @@ -1,15 +1,33 @@
- +
+
+
+ +
+ {{ $ctrl.titleText }} +
+ +
+ +
+
-
-
- -
-
-
-
- -
-
_.includes(ids, tag.Id)); + const filteredTags = _.filter(tags, (tag) => _.includes(ids, tag.ID)); const tagNames = _.map(filteredTags, 'Name'); return tagNames; } diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx index 310c25667..c446c3e1b 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx @@ -1,10 +1,10 @@ import { Zap } from 'react-feather'; -import { EnvironmentType } from '@/portainer/environments/types'; +import { EnvironmentType } from '@/react/portainer/environments/types'; import { isAgentEnvironment, isEdgeEnvironment, -} from '@/portainer/environments/utils'; +} from '@/react/portainer/environments/utils'; interface Props { type: EnvironmentType; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.tsx index 74ee343f9..0df4641e9 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import { isoDateFromTimestamp } from '@/portainer/filters/filters'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { usePublicSettings } from '@/react/portainer/settings/queries'; import { PublicSettingsViewModel } from '@/portainer/models/settings'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx index d301f3d93..69b79c22a 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentIcon.tsx @@ -4,7 +4,7 @@ import { environmentTypeIcon } from '@/portainer/filters/filters'; import dockerEdge from '@/assets/images/edge_endpoint.png'; import kube from '@/assets/images/kubernetes_endpoint.png'; import kubeEdge from '@/assets/images/kubernetes_edge_endpoint.png'; -import { EnvironmentType } from '@/portainer/environments/types'; +import { EnvironmentType } from '@/react/portainer/environments/types'; import azure from '@/assets/ico/vendor/azure.svg'; import docker from '@/assets/ico/vendor/docker.svg'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx index 89fc50974..c42d05751 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.stories.tsx @@ -4,7 +4,7 @@ import { Environment, EnvironmentStatus, EnvironmentType, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { createMockEnvironment } from '@/react-tools/test-mocks'; import { EnvironmentItem } from './EnvironmentItem'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx index 826d97e10..3fb17b7af 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx @@ -2,7 +2,7 @@ import { EnvironmentGroup, EnvironmentGroupId, } from '@/portainer/environment-groups/types'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { UserContext } from '@/portainer/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; import { Tag } from '@/portainer/tags/types'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx index 892e2d5f1..8215cb1aa 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx @@ -7,12 +7,15 @@ import { humanize, stripProtocol, } from '@/portainer/filters/filters'; -import { type Environment, PlatformType } from '@/portainer/environments/types'; +import { + type Environment, + PlatformType, +} from '@/react/portainer/environments/types'; import { getPlatformType, isDockerEnvironment, isEdgeEnvironment, -} from '@/portainer/environments/utils'; +} from '@/react/portainer/environments/utils'; import type { TagId } from '@/portainer/tags/types'; import { useTags } from '@/portainer/tags/queries'; import { useUser } from '@/portainer/hooks/useUser'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStats.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStats.tsx index 1391e8c86..ad4f7d20e 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStats.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStats.tsx @@ -1,5 +1,8 @@ -import { Environment, PlatformType } from '@/portainer/environments/types'; -import { getPlatformType } from '@/portainer/environments/utils'; +import { + Environment, + PlatformType, +} from '@/react/portainer/environments/types'; +import { getPlatformType } from '@/react/portainer/environments/utils'; import { EnvironmentStatsDocker } from './EnvironmentStatsDocker'; import { EnvironmentStatsKubernetes } from './EnvironmentStatsKubernetes'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx index d0e768b23..d4a4d4161 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx @@ -1,7 +1,7 @@ import { DockerSnapshot, EnvironmentType, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { addPlural } from '@/portainer/helpers/strings'; import { AgentVersionTag } from './AgentVersionTag'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsKubernetes.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsKubernetes.tsx index 8881eb6e3..c43f78efd 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsKubernetes.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatsKubernetes.tsx @@ -1,7 +1,7 @@ import { EnvironmentType, KubernetesSnapshot, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { humanize } from '@/portainer/filters/filters'; import { addPlural } from '@/portainer/helpers/strings'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatusBadge.tsx b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatusBadge.tsx index 778d88779..482cf952f 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatusBadge.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentItem/EnvironmentStatusBadge.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; -import { EnvironmentStatus } from '@/portainer/environments/types'; +import { EnvironmentStatus } from '@/react/portainer/environments/types'; interface Props { status: EnvironmentStatus; diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.test.tsx b/app/portainer/home/EnvironmentList/EnvironmentList.test.tsx index 8db69b86c..9cdf468d8 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentList.test.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentList.test.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { UserContext } from '@/portainer/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; import { renderWithQueryClient } from '@/react-tools/test-utils'; diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.tsx b/app/portainer/home/EnvironmentList/EnvironmentList.tsx index 0cde258dd..267651189 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentList.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentList.tsx @@ -10,7 +10,7 @@ import { EnvironmentStatus, PlatformType, EdgeTypes, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { EnvironmentGroupId } from '@/portainer/environment-groups/types'; import { HomepageFilter, @@ -20,12 +20,12 @@ import { useDebounce } from '@/portainer/hooks/useDebounce'; import { refetchIfAnyOffline, useEnvironmentList, -} from '@/portainer/environments/queries/useEnvironmentList'; +} from '@/react/portainer/environments/queries/useEnvironmentList'; import { useGroups } from '@/portainer/environment-groups/queries'; import { useTags } from '@/portainer/tags/queries'; import { Filter } from '@/portainer/home/types'; -import { useAgentVersionsList } from '@/portainer/environments/queries/useAgentVersionsList'; -import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service'; +import { useAgentVersionsList } from '@/react/portainer/environments/queries/useAgentVersionsList'; +import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service'; import { useUser } from '@/portainer/hooks/useUser'; import { TableFooter } from '@@/datatables/TableFooter'; diff --git a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx index 6d8002b5a..265a5c025 100644 --- a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx +++ b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { Download } from 'react-feather'; -import { Environment } from '@/portainer/environments/types'; -import { isKubernetesEnvironment } from '@/portainer/environments/utils'; +import { Environment } from '@/react/portainer/environments/types'; +import { isKubernetesEnvironment } from '@/react/portainer/environments/utils'; import { trackEvent } from '@/angulartics.matomo/analytics-services'; -import { Query } from '@/portainer/environments/queries/useEnvironmentList'; +import { Query } from '@/react/portainer/environments/queries/useEnvironmentList'; import { Button } from '@@/buttons'; diff --git a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigPrompt.tsx b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigPrompt.tsx index 865e05f64..5349ad733 100644 --- a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigPrompt.tsx +++ b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigPrompt.tsx @@ -5,13 +5,13 @@ import { DialogOverlay } from '@reach/dialog'; import * as kcService from '@/kubernetes/services/kubeconfig.service'; import * as notifications from '@/portainer/services/notifications'; -import { EnvironmentType } from '@/portainer/environments/types'; +import { EnvironmentType } from '@/react/portainer/environments/types'; import { usePaginationLimitState } from '@/portainer/hooks/usePaginationLimitState'; import { usePublicSettings } from '@/react/portainer/settings/queries'; import { Query, useEnvironmentList, -} from '@/portainer/environments/queries/useEnvironmentList'; +} from '@/react/portainer/environments/queries/useEnvironmentList'; import { PaginationControls } from '@@/PaginationControls'; import { Checkbox } from '@@/form-components/Checkbox'; diff --git a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigSelection.ts b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigSelection.ts index ee4b07ba8..a3ac3a23c 100644 --- a/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigSelection.ts +++ b/app/portainer/home/EnvironmentList/KubeconfigButton/KubeconfigSelection.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; export function useSelection() { const [selection, setSelection] = useState>( diff --git a/app/portainer/home/HomeView.tsx b/app/portainer/home/HomeView.tsx index d6121b707..d1e656f56 100644 --- a/app/portainer/home/HomeView.tsx +++ b/app/portainer/home/HomeView.tsx @@ -1,12 +1,13 @@ import { useRouter } from '@uirouter/react'; import { useState } from 'react'; +import { Environment } from '@/react/portainer/environments/types'; +import { snapshotEndpoints } from '@/react/portainer/environments/environment.service'; +import { isEdgeEnvironment } from '@/react/portainer/environments/utils'; + import { PageHeader } from '@@/PageHeader'; import * as notifications from '../services/notifications'; -import { Environment } from '../environments/types'; -import { snapshotEndpoints } from '../environments/environment.service'; -import { isEdgeEnvironment } from '../environments/utils'; import { confirmAsync } from '../services/modal.service/confirm'; import { buildTitle } from '../services/modal.service/utils'; diff --git a/app/portainer/hooks/useCurrentEnvironment.ts b/app/portainer/hooks/useCurrentEnvironment.ts index 714dc282d..70a2c9ada 100644 --- a/app/portainer/hooks/useCurrentEnvironment.ts +++ b/app/portainer/hooks/useCurrentEnvironment.ts @@ -1,4 +1,4 @@ -import { useEnvironment } from '../environments/queries/useEnvironment'; +import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment'; import { useEnvironmentId } from './useEnvironmentId'; diff --git a/app/portainer/hooks/useUser.tsx b/app/portainer/hooks/useUser.tsx index dccbef18d..9c9d8517c 100644 --- a/app/portainer/hooks/useUser.tsx +++ b/app/portainer/hooks/useUser.tsx @@ -11,10 +11,10 @@ import { } from 'react'; import { isAdmin } from '@/portainer/users/user.helpers'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { getUser } from '../users/user.service'; import { User, UserId } from '../users/types'; -import { EnvironmentId } from '../environments/types'; import { useLocalStorage } from './useLocalStorage'; @@ -123,6 +123,7 @@ interface AuthorizedProps { authorizations: string | string[]; environmentId?: EnvironmentId; adminOnlyCE?: boolean; + childrenUnauthorized?: ReactNode; } export function Authorized({ @@ -130,6 +131,7 @@ export function Authorized({ environmentId, adminOnlyCE = false, children, + childrenUnauthorized = null, }: PropsWithChildren) { const isAllowed = useAuthorizations( authorizations, @@ -137,7 +139,7 @@ export function Authorized({ adminOnlyCE ); - return isAllowed ? <>{children} : null; + return isAllowed ? <>{children} : <>{childrenUnauthorized}; } interface UserProviderProps { diff --git a/app/portainer/hostmanagement/open-amt/open-amt.service.ts b/app/portainer/hostmanagement/open-amt/open-amt.service.ts index 378143bd2..eabd14a66 100644 --- a/app/portainer/hostmanagement/open-amt/open-amt.service.ts +++ b/app/portainer/hostmanagement/open-amt/open-amt.service.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { OpenAMTConfiguration, diff --git a/app/portainer/oauth/components/oauth-providers-selector/oauth-options.tsx b/app/portainer/oauth/components/oauth-providers-selector/oauth-options.tsx index 38f9a5197..8bfc61df8 100644 --- a/app/portainer/oauth/components/oauth-providers-selector/oauth-options.tsx +++ b/app/portainer/oauth/components/oauth-providers-selector/oauth-options.tsx @@ -5,7 +5,7 @@ import Microsoft from '@/assets/ico/vendor/microsoft.svg?c'; import Google from '@/assets/ico/vendor/google.svg?c'; import Github from '@/assets/ico/vendor/github.svg?c'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; export const options = [ { diff --git a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js index bd20c318f..01d97fd81 100644 --- a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js +++ b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; import AccessViewerPolicyModel from '../../models/access'; export default class AccessViewerController { diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts index e2f2e2410..14eeff6f0 100644 --- a/app/portainer/react/components/index.ts +++ b/app/portainer/react/components/index.ts @@ -30,7 +30,7 @@ import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell'; import { DashboardItem } from '@@/DashboardItem'; import { SearchBar } from '@@/datatables/SearchBar'; import { FallbackImage } from '@@/FallbackImage'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; import { TeamsSelector } from '@@/TeamsSelector'; import { PortainerSelect } from '@@/form-components/PortainerSelect'; @@ -88,8 +88,6 @@ export const componentsModule = angular 'alt', 'size', 'className', - 'fallbackMode', - 'fallbackClassName', 'feather', ]) ) @@ -106,10 +104,10 @@ export const componentsModule = angular 'datatableSearchbar', r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder']) ) - .component('boxSelectorBadgeIcon', r2a(BadgeIcon, ['featherIcon', 'icon'])) + .component('badgeIcon', r2a(BadgeIcon, ['featherIcon', 'icon', 'size'])) .component( 'accessControlPanel', - r2a(withReactQuery(withCurrentUser(AccessControlPanel)), [ + r2a(withUIRouter(withReactQuery(withCurrentUser(AccessControlPanel))), [ 'disableOwnershipChange', 'onUpdateSuccess', 'resourceControl', @@ -149,6 +147,7 @@ export const componentsModule = angular 'name', 'placeholder', 'teams', + 'disabled', ]) ) .component( diff --git a/app/portainer/services/nameValidator.js b/app/portainer/services/nameValidator.js index fa4f3fe2e..bf3ca58d2 100644 --- a/app/portainer/services/nameValidator.js +++ b/app/portainer/services/nameValidator.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import { getEnvironments } from '../environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; angular.module('portainer.app').factory('NameValidator', NameValidatorFactory); /* @ngInject */ diff --git a/app/portainer/services/types.ts b/app/portainer/services/types.ts index a4e17ef59..af8895e80 100644 --- a/app/portainer/services/types.ts +++ b/app/portainer/services/types.ts @@ -1,4 +1,4 @@ -import { Environment } from '../environments/types'; +import { Environment } from '@/react/portainer/environments/types'; export interface EndpointProvider { setEndpointID(id: Environment['Id']): void; diff --git a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-options.tsx b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-options.tsx index 9b381bda3..6b44625f6 100644 --- a/app/portainer/settings/authentication/ldap/ldap-settings/ldap-options.tsx +++ b/app/portainer/settings/authentication/ldap/ldap-settings/ldap-options.tsx @@ -3,7 +3,7 @@ import { Edit } from 'react-feather'; import { FeatureId } from '@/portainer/feature-flags/enums'; import Openldap from '@/assets/ico/vendor/openldap.svg?c'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; const SERVER_TYPES = { CUSTOM: 0, diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts index b51673f20..d92b4d5e3 100644 --- a/app/portainer/users/types.ts +++ b/app/portainer/users/types.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '../environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; export type UserId = number; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 56e7ac9a3..da92669ae 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -1,6 +1,6 @@ import angular from 'angular'; import uuidv4 from 'uuid/v4'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; class AuthenticationController { /* @ngInject */ diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html index 0c9643c9a..625d5ac1e 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html @@ -83,9 +83,6 @@ {{ $ctrl.formValues.File.name }} - - -
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 4ea9ef956..54c064201 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -6,7 +6,7 @@ import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurit import EndpointHelper from '@/portainer/helpers/endpointHelper'; import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service'; import { confirmDestructiveAsync } from '@/portainer/services/modal.service/confirm'; -import { isEdgeEnvironment } from '@/portainer/environments/utils'; +import { isEdgeEnvironment } from '@/react/portainer/environments/utils'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; import { GpusListAngular } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index 4bf3c6f82..07f09ed62 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -1,6 +1,6 @@ import { map } from 'lodash'; import EndpointHelper from '@/portainer/helpers/endpointHelper'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; export class EndpointsController { /* @ngInject */ diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index a2940115a..4783c10ab 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -1,4 +1,4 @@ -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; angular.module('portainer.app').controller('InitAdminController', [ '$scope', diff --git a/app/portainer/views/registries/create/options.tsx b/app/portainer/views/registries/create/options.tsx index e1045c0db..ddf7bd9b5 100644 --- a/app/portainer/views/registries/create/options.tsx +++ b/app/portainer/views/registries/create/options.tsx @@ -7,7 +7,7 @@ import Proget from '@/assets/ico/vendor/proget.svg?c'; import Azure from '@/assets/ico/vendor/azure.svg?c'; import Gitlab from '@/assets/ico/vendor/gitlab.svg?c'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; export const options = [ { diff --git a/app/portainer/views/settings/authentication/options.tsx b/app/portainer/views/settings/authentication/options.tsx index 08f024cd9..01141cf2e 100644 --- a/app/portainer/views/settings/authentication/options.tsx +++ b/app/portainer/views/settings/authentication/options.tsx @@ -5,7 +5,7 @@ import Microsoft from '@/assets/ico/vendor/microsoft.svg?c'; import Ldap from '@/assets/ico/ldap.svg?c'; import OAuth from '@/assets/ico/oauth.svg?c'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; export const options = [ { diff --git a/app/portainer/views/settings/options.tsx b/app/portainer/views/settings/options.tsx index 6cf2c413d..45fc6e3b7 100644 --- a/app/portainer/views/settings/options.tsx +++ b/app/portainer/views/settings/options.tsx @@ -2,7 +2,7 @@ import { DownloadCloud, UploadCloud } from 'react-feather'; import { FeatureId } from '@/portainer/feature-flags/enums'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; export const options = [ { diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index e9bcd2c9b..13783eba0 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -1,7 +1,7 @@ import { ResourceControlType } from '@/react/portainer/access-control/types'; import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; import { FeatureId } from 'Portainer/feature-flags/enums'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; import { StackStatus, StackType } from '@/react/docker/stacks/types'; angular.module('portainer.app').controller('StackController', [ diff --git a/app/portainer/views/templates/templates.html b/app/portainer/views/templates/templates.html index 90571ca3d..e1531b4ae 100644 --- a/app/portainer/views/templates/templates.html +++ b/app/portainer/views/templates/templates.html @@ -105,7 +105,11 @@
-
Add map additional port
+
+ + Add map additional port + +
@@ -171,7 +175,11 @@
-
Add map additional volume
+
+ + Add map additional volume + +
@@ -191,7 +199,11 @@ -
Add additional entry
+
+ + Add additional entry + +
@@ -216,7 +228,11 @@ -
Add label
+
+ + Add label + +
diff --git a/app/portainer/views/users/users.html b/app/portainer/views/users/users.html index 97422fd2c..30a148024 100644 --- a/app/portainer/views/users/users.html +++ b/app/portainer/views/users/users.html @@ -118,9 +118,20 @@ data-cy="'user-teamSelect'" on-change="(onChangeTeamIds)" input-id="'teams-selector'" + disabled="teamSync" > +
+
+ +

+ + The team leader feature is disabled as external authentication is currently enabled with team sync. +

+
+
+
diff --git a/app/portainer/views/users/usersController.js b/app/portainer/views/users/usersController.js index d3a35547c..bd326a767 100644 --- a/app/portainer/views/users/usersController.js +++ b/app/portainer/views/users/usersController.js @@ -131,6 +131,7 @@ angular.module('portainer.app').controller('UsersController', [ $scope.teams = _.orderBy(data.teams, 'Name', 'asc'); $scope.AuthenticationMethod = data.settings.AuthenticationMethod; $scope.requiredPasswordLength = data.settings.RequiredPasswordLength; + $scope.teamSync = data.settings.TeamSync; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve users and teams'); diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 59e8fddb3..43dacc1aa 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { Team } from '@/react/portainer/users/teams/types'; import { Role, User, UserId } from '@/portainer/users/types'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; export function createMockUsers( count: number, @@ -75,6 +75,7 @@ export function createMockEnvironment(): Environment { Configuration: { IngressClasses: [], IngressAvailabilityPerNamespace: false, + AllowNoneIngressClass: false, }, }, EdgeKey: '', diff --git a/app/react/azure/container-instances/CreateView/useCreateInstanceMutation.tsx b/app/react/azure/container-instances/CreateView/useCreateInstanceMutation.tsx index 040508019..db8029f77 100644 --- a/app/react/azure/container-instances/CreateView/useCreateInstanceMutation.tsx +++ b/app/react/azure/container-instances/CreateView/useCreateInstanceMutation.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from 'react-query'; import { createContainerGroup } from '@/react/azure/services/container-groups.service'; import { queryKeys } from '@/react/azure/queries/query-keys'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import PortainerError from '@/portainer/error'; import { ContainerGroup, diff --git a/app/react/azure/container-instances/CreateView/useLoadFormState.ts b/app/react/azure/container-instances/CreateView/useLoadFormState.ts index 4214271e8..416944433 100644 --- a/app/react/azure/container-instances/CreateView/useLoadFormState.ts +++ b/app/react/azure/container-instances/CreateView/useLoadFormState.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { ContainerInstanceFormValues, ProviderViewModel, diff --git a/app/react/azure/container-instances/ListView/ListView.tsx b/app/react/azure/container-instances/ListView/ListView.tsx index 8fbe4e4cf..3698ffda3 100644 --- a/app/react/azure/container-instances/ListView/ListView.tsx +++ b/app/react/azure/container-instances/ListView/ListView.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from 'react-query'; import { deleteContainerGroup } from '@/react/azure/services/container-groups.service'; import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { promiseSequence } from '@/portainer/helpers/promise-utils'; import { useContainerGroups } from '@/react/azure/queries/useContainerGroups'; import { useSubscriptions } from '@/react/azure/queries/useSubscriptions'; diff --git a/app/react/azure/queries/query-keys.ts b/app/react/azure/queries/query-keys.ts index 06a754c90..bd5288078 100644 --- a/app/react/azure/queries/query-keys.ts +++ b/app/react/azure/queries/query-keys.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; export const queryKeys = { subscriptions: (environmentId: EnvironmentId) => diff --git a/app/react/azure/queries/useContainerGroup.ts b/app/react/azure/queries/useContainerGroup.ts index 1e3ad2656..8baedb37e 100644 --- a/app/react/azure/queries/useContainerGroup.ts +++ b/app/react/azure/queries/useContainerGroup.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { withError } from '@/react-tools/react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; diff --git a/app/react/azure/queries/useContainerGroups.ts b/app/react/azure/queries/useContainerGroups.ts index 16c7f4a2f..80a02806d 100644 --- a/app/react/azure/queries/useContainerGroups.ts +++ b/app/react/azure/queries/useContainerGroups.ts @@ -4,7 +4,7 @@ import { useQueries } from 'react-query'; import { withError } from '@/react-tools/react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Subscription, ContainerGroup } from '../types'; diff --git a/app/react/azure/queries/useProvider.ts b/app/react/azure/queries/useProvider.ts index 1b1e5bd23..497a2910d 100644 --- a/app/react/azure/queries/useProvider.ts +++ b/app/react/azure/queries/useProvider.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { useQueries } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { withError } from '@/react-tools/react-query'; diff --git a/app/react/azure/queries/useResourceGroup.ts b/app/react/azure/queries/useResourceGroup.ts index 07a307c0d..0a7e0eba9 100644 --- a/app/react/azure/queries/useResourceGroup.ts +++ b/app/react/azure/queries/useResourceGroup.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { withError } from '@/react-tools/react-query'; diff --git a/app/react/azure/queries/useResourceGroups.ts b/app/react/azure/queries/useResourceGroups.ts index e49de14a4..33832e628 100644 --- a/app/react/azure/queries/useResourceGroups.ts +++ b/app/react/azure/queries/useResourceGroups.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { useQueries } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { withError } from '@/react-tools/react-query'; diff --git a/app/react/azure/queries/useSubscription.ts b/app/react/azure/queries/useSubscription.ts index 7e0148e9a..5cfe9cb0c 100644 --- a/app/react/azure/queries/useSubscription.ts +++ b/app/react/azure/queries/useSubscription.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { withError } from '@/react-tools/react-query'; diff --git a/app/react/azure/queries/useSubscriptions.ts b/app/react/azure/queries/useSubscriptions.ts index bb2b32cde..e5fbb587d 100644 --- a/app/react/azure/queries/useSubscriptions.ts +++ b/app/react/azure/queries/useSubscriptions.ts @@ -1,7 +1,7 @@ import { useQuery } from 'react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { withError } from '@/react-tools/react-query'; import { azureErrorParser } from '../services/utils'; diff --git a/app/react/azure/queries/utils.ts b/app/react/azure/queries/utils.ts index 599ae7c8f..c16495d95 100644 --- a/app/react/azure/queries/utils.ts +++ b/app/react/azure/queries/utils.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; export function buildSubscriptionsUrl( environmentId: EnvironmentId, diff --git a/app/react/azure/services/container-groups.service.ts b/app/react/azure/services/container-groups.service.ts index 38c095230..a95ff480b 100644 --- a/app/react/azure/services/container-groups.service.ts +++ b/app/react/azure/services/container-groups.service.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { buildContainerGroupUrl } from '../queries/utils'; diff --git a/app/react/components/BadgeIcon/Badge.stories.tsx b/app/react/components/BadgeIcon/Badge.stories.tsx new file mode 100644 index 000000000..4a1b428d5 --- /dev/null +++ b/app/react/components/BadgeIcon/Badge.stories.tsx @@ -0,0 +1,35 @@ +import { Meta } from '@storybook/react'; + +import { BadgeIcon, BadgeSize, Props } from './BadgeIcon'; + +export default { + component: BadgeIcon, + title: 'Components/BadgeIcon', + argTypes: { + size: { + control: { + type: 'select', + options: ['md', 'lg', 'xl', '2xl', '3xl'], + }, + }, + icon: { + control: { + type: 'select', + options: ['edit', 'info', 'smile', 'users'], + }, + }, + }, +} as Meta; + +// : JSX.IntrinsicAttributes & PropsWithChildren +function Template({ + size = '3xl', + icon = 'edit', +}: { + size?: BadgeSize; + icon: string; +}) { + return ; +} + +export const Example = Template.bind({}); diff --git a/app/react/components/BadgeIcon/BadgeIcon.tsx b/app/react/components/BadgeIcon/BadgeIcon.tsx new file mode 100644 index 000000000..83b198bcc --- /dev/null +++ b/app/react/components/BadgeIcon/BadgeIcon.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx'; + +import { Icon, IconProps } from '@@/Icon'; + +export type BadgeSize = 'md' | 'lg' | 'xl' | '2xl' | '3xl'; + +export interface Props extends IconProps { + size?: BadgeSize; +} + +export function BadgeIcon({ icon, featherIcon, size = '3xl' }: Props) { + const sizeClasses = iconSizeToClasses(size); + return ( +
+ +
+ ); +} + +function iconSizeToClasses(size: BadgeSize) { + switch (size) { + case 'md': + return 'h-6 w-6 text-md'; + case 'lg': + return 'h-8 w-8 text-lg'; + case 'xl': + return 'h-10 w-10 text-xl'; + case '2xl': + return 'h-12 w-12 text-2xl'; + case '3xl': + return 'h-14 w-14 text-3xl'; + default: + return 'h-14 w-14 text-3xl'; + } +} diff --git a/app/react/components/BadgeIcon/index.ts b/app/react/components/BadgeIcon/index.ts new file mode 100644 index 000000000..e0138de42 --- /dev/null +++ b/app/react/components/BadgeIcon/index.ts @@ -0,0 +1 @@ +export { BadgeIcon } from './BadgeIcon'; diff --git a/app/react/components/BoxSelector/BadgeIcon.tsx b/app/react/components/BoxSelector/BadgeIcon.tsx deleted file mode 100644 index 1e3818964..000000000 --- a/app/react/components/BoxSelector/BadgeIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Icon, IconProps } from '@@/Icon'; - -type Props = IconProps; - -export function BadgeIcon({ icon, featherIcon }: Props) { - return ( -
- -
- ); -} diff --git a/app/react/components/BoxSelector/common-options/build-methods.tsx b/app/react/components/BoxSelector/common-options/build-methods.tsx index 7c6f267d7..4b3dc96d3 100644 --- a/app/react/components/BoxSelector/common-options/build-methods.tsx +++ b/app/react/components/BoxSelector/common-options/build-methods.tsx @@ -2,7 +2,8 @@ import { Edit, FileText, Globe, Upload } from 'react-feather'; import GitIcon from '@/assets/ico/git.svg?c'; -import { BadgeIcon } from '../BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; + import { BoxSelectorOption } from '../types'; export const editor: BoxSelectorOption<'editor'> = { diff --git a/app/react/components/FallbackImage.tsx b/app/react/components/FallbackImage.tsx index 1e11c294d..a48cd491b 100644 --- a/app/react/components/FallbackImage.tsx +++ b/app/react/components/FallbackImage.tsx @@ -1,18 +1,15 @@ -import clsx from 'clsx'; import { useEffect, useState } from 'react'; -import { Icon, IconMode, IconSize } from './Icon'; +import { BadgeIcon, BadgeSize } from './BadgeIcon/BadgeIcon'; interface Props { // props for the image to load src: string; // a link to an external image fallbackIcon: string; alt?: string; - size?: IconSize; + size?: BadgeSize; className?: string; - // additional fallback icon props - fallbackMode?: IconMode; - fallbackClassName?: string; + // additional fallback badge props feather?: boolean; } @@ -22,22 +19,18 @@ export function FallbackImage({ alt, size, className, - fallbackMode, - fallbackClassName, feather, }: Props) { const [error, setError] = useState(false); - const classes = clsx(className, { [`icon-${size}`]: size }); - useEffect(() => { setError(false); }, [src]); - if (!error) { + if (!error && src) { return ( {alt} setError(true)} @@ -46,13 +39,5 @@ export function FallbackImage({ } // fallback icon if there is an error loading the image - return ( - - ); + return ; } diff --git a/app/react/components/TeamsSelector/TeamsSelector.tsx b/app/react/components/TeamsSelector/TeamsSelector.tsx index b19532140..0158b8b06 100644 --- a/app/react/components/TeamsSelector/TeamsSelector.tsx +++ b/app/react/components/TeamsSelector/TeamsSelector.tsx @@ -10,6 +10,7 @@ interface Props { dataCy?: string; inputId?: string; placeholder?: string; + disabled?: boolean; } export function TeamsSelector({ @@ -20,6 +21,7 @@ export function TeamsSelector({ dataCy, inputId, placeholder, + disabled, }: Props) { const options = teams.map((team) => ({ label: team.Name, value: team.Id })); @@ -33,6 +35,7 @@ export function TeamsSelector({ data-cy={dataCy} inputId={inputId} placeholder={placeholder} + disabled={disabled} /> ); } diff --git a/app/react/components/UsersSelector/UsersSelector.tsx b/app/react/components/UsersSelector/UsersSelector.tsx index 2963830e8..4edb68028 100644 --- a/app/react/components/UsersSelector/UsersSelector.tsx +++ b/app/react/components/UsersSelector/UsersSelector.tsx @@ -10,6 +10,7 @@ interface Props { dataCy?: string; inputId?: string; placeholder?: string; + disabled?: boolean; } export function UsersSelector({ @@ -20,6 +21,7 @@ export function UsersSelector({ dataCy, inputId, placeholder, + disabled, }: Props) { return ( item.Name} diff --git a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx index d1a864acd..c84875285 100644 --- a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx +++ b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails'; +import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; import { Code } from '@@/Code'; import { CopyButton } from '@@/buttons/CopyButton'; diff --git a/app/react/edge/edge-groups/types.ts b/app/react/edge/edge-groups/types.ts index d08cb3282..c60778ea7 100644 --- a/app/react/edge/edge-groups/types.ts +++ b/app/react/edge/edge-groups/types.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { TagId } from '@/portainer/tags/types'; export interface EdgeGroup { diff --git a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx index 2412e0c29..2d71df02c 100644 --- a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx +++ b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { confirmWarn } from '@/portainer/services/modal.service/confirm'; @@ -14,29 +14,64 @@ import { createStore } from './datatable-store'; const useStore = createStore('ingressClasses'); interface Props { - onChangeAvailability: ( + onChangeControllers: ( controllerClassMap: IngressControllerClassMap[] ) => void; // angular function to save the ingress class list description: string; ingressControllers: IngressControllerClassMap[] | undefined; + allowNoneIngressClass: boolean; isLoading: boolean; noIngressControllerLabel: string; view: string; } export function IngressClassDatatable({ - onChangeAvailability, + onChangeControllers, description, ingressControllers, + allowNoneIngressClass, isLoading, noIngressControllerLabel, view, }: Props) { - const [ingControllerFormValues, setIngControllerFormValues] = - useState(ingressControllers); + const [ingControllerFormValues, setIngControllerFormValues] = useState( + ingressControllers || [] + ); const settings = useStore(); const columns = useColumns(); + useEffect(() => { + if (allowNoneIngressClass === undefined) { + return; + } + + let newIngFormValues: IngressControllerClassMap[]; + const isCustomTypeExist = ingControllerFormValues.some( + (ic) => ic.Type === 'custom' + ); + if (allowNoneIngressClass) { + newIngFormValues = [...ingControllerFormValues]; + // add the ingress controller type 'custom' with a 'none' ingress class name + if (!isCustomTypeExist) { + newIngFormValues.push({ + Name: 'none', + ClassName: 'none', + Type: 'custom', + Availability: true, + New: false, + Used: false, + }); + } + } else { + newIngFormValues = ingControllerFormValues.filter( + (ingController) => ingController.ClassName !== 'none' + ); + } + setIngControllerFormValues(newIngFormValues); + onChangeControllers(newIngFormValues); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allowNoneIngressClass, onChangeControllers]); + return (
{ if (confirmed) { setIngControllerFormValues(updatedIngressControllers); - onChangeAvailability(updatedIngressControllers); + onChangeControllers(updatedIngressControllers); } }, }); return; } setIngControllerFormValues(updatedIngressControllers); - onChangeAvailability(updatedIngressControllers); + onChangeControllers(updatedIngressControllers); } } } @@ -196,10 +231,13 @@ function isUnsavedChanges( oldIngressControllers: IngressControllerClassMap[], newIngressControllers: IngressControllerClassMap[] ) { - for (let i = 0; i < oldIngressControllers.length; i += 1) { + if (oldIngressControllers.length !== newIngressControllers.length) { + return true; + } + for (let i = 0; i < newIngressControllers.length; i += 1) { if ( - oldIngressControllers[i].Availability !== - newIngressControllers[i].Availability + oldIngressControllers[i]?.Availability !== + newIngressControllers[i]?.Availability ) { return true; } diff --git a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/RowContext.tsx b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/RowContext.tsx index c75f3e3e8..66af35873 100644 --- a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/RowContext.tsx +++ b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/RowContext.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { createRowContext } from '@@/datatables/RowContext'; diff --git a/app/react/kubernetes/cluster/ingressClass/ingressClass.service.ts b/app/react/kubernetes/cluster/ingressClass/ingressClass.service.ts index 50ebeb606..dc1d5e6bb 100644 --- a/app/react/kubernetes/cluster/ingressClass/ingressClass.service.ts +++ b/app/react/kubernetes/cluster/ingressClass/ingressClass.service.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { KubernetesApiListResponse, V1IngressClass, diff --git a/app/react/kubernetes/cluster/ingressClass/types.ts b/app/react/kubernetes/cluster/ingressClass/types.ts index c085bc95f..9e981ef03 100644 --- a/app/react/kubernetes/cluster/ingressClass/types.ts +++ b/app/react/kubernetes/cluster/ingressClass/types.ts @@ -1,9 +1,13 @@ -type SupportedIngControllerNames = 'nginx' | 'traefik' | 'unknown'; +export type SupportedIngControllerTypes = + | 'nginx' + | 'traefik' + | 'other' + | 'custom'; export interface IngressControllerClassMap extends Record { Name: string; ClassName: string; - Type: SupportedIngControllerNames; + Type: SupportedIngControllerTypes; Availability: boolean; New: boolean; Used: boolean; // if the controller is used by any ingress in the cluster diff --git a/app/react/kubernetes/cluster/ingressClass/utils/index.ts b/app/react/kubernetes/cluster/ingressClass/utils/index.ts index 8b462a37c..6f5949222 100644 --- a/app/react/kubernetes/cluster/ingressClass/utils/index.ts +++ b/app/react/kubernetes/cluster/ingressClass/utils/index.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import PortainerError from '@/portainer/error'; import axios from '@/portainer/services/axios'; diff --git a/app/react/kubernetes/configs/queries.ts b/app/react/kubernetes/configs/queries.ts index e45253366..ad6cb4ef2 100644 --- a/app/react/kubernetes/configs/queries.ts +++ b/app/react/kubernetes/configs/queries.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { error as notifyError } from '@/portainer/services/notifications'; import { getConfigMaps } from './service'; diff --git a/app/react/kubernetes/configs/service.ts b/app/react/kubernetes/configs/service.ts index 7ebc55ced..cd3a4c6b6 100644 --- a/app/react/kubernetes/configs/service.ts +++ b/app/react/kubernetes/configs/service.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Configuration } from './types'; diff --git a/app/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector.tsx b/app/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector.tsx index 88d41847a..423d37de7 100644 --- a/app/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector.tsx +++ b/app/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector.tsx @@ -1,4 +1,4 @@ -import { Registry } from '@/portainer/environments/environment.service/registries'; +import { Registry } from '@/react/portainer/environments/environment.service/registries'; import { Select } from '@@/form-components/ReactSelect'; diff --git a/app/react/kubernetes/namespaces/queries.ts b/app/react/kubernetes/namespaces/queries.ts index 13657f4ea..f79a6c555 100644 --- a/app/react/kubernetes/namespaces/queries.ts +++ b/app/react/kubernetes/namespaces/queries.ts @@ -1,14 +1,30 @@ import { useQuery } from 'react-query'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { error as notifyError } from '@/portainer/services/notifications'; +import { getIngresses } from '@/kubernetes/react/views/networks/ingresses/service'; import { getNamespaces, getNamespace } from './service'; +import { Namespaces } from './types'; export function useNamespaces(environmentId: EnvironmentId) { return useQuery( ['environments', environmentId, 'kubernetes', 'namespaces'], - () => getNamespaces(environmentId), + async () => { + const namespaces = await getNamespaces(environmentId); + const settledNamespacesPromise = await Promise.allSettled( + Object.keys(namespaces).map((namespace) => + getIngresses(environmentId, namespace).then(() => namespace) + ) + ); + const ns: Namespaces = {}; + settledNamespacesPromise.forEach((namespace) => { + if (namespace.status === 'fulfilled') { + ns[namespace.value] = namespaces[namespace.value]; + } + }); + return ns; + }, { onError: (err) => { notifyError('Failure', err as Error, 'Unable to get namespaces.'); diff --git a/app/react/kubernetes/namespaces/service.ts b/app/react/kubernetes/namespaces/service.ts index 74cd14e0d..57e91689d 100644 --- a/app/react/kubernetes/namespaces/service.ts +++ b/app/react/kubernetes/namespaces/service.ts @@ -1,5 +1,5 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Namespaces } from './types'; @@ -8,23 +8,23 @@ export async function getNamespace( namespace: string ) { try { - const { data: ingress } = await axios.get( + const { data: ns } = await axios.get( buildUrl(environmentId, namespace) ); - return ingress; + return ns; } catch (e) { - throw parseAxiosError(e as Error, 'Unable to retrieve network details'); + throw parseAxiosError(e as Error, 'Unable to retrieve namespace'); } } export async function getNamespaces(environmentId: EnvironmentId) { try { - const { data: ingresses } = await axios.get( + const { data: namespaces } = await axios.get( buildUrl(environmentId) ); - return ingresses; + return namespaces; } catch (e) { - throw parseAxiosError(e as Error, 'Unable to retrieve network details'); + throw parseAxiosError(e as Error, 'Unable to retrieve namespaces'); } } diff --git a/app/react/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx b/app/react/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx index 93fab6cff..3742dd426 100644 --- a/app/react/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx +++ b/app/react/portainer/access-control/AccessControlPanel/AccessControlPanel.tsx @@ -4,7 +4,7 @@ import { useUser } from '@/portainer/hooks/useUser'; import { Icon } from '@/react/components/Icon'; import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types'; import { useUserMembership } from '@/portainer/users/queries'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { TableContainer, TableTitle } from '@@/datatables'; import { Button } from '@@/buttons'; diff --git a/app/react/portainer/access-control/AccessControlPanel/AccessControlPanelForm.tsx b/app/react/portainer/access-control/AccessControlPanel/AccessControlPanelForm.tsx index 78068c89e..de6b64af4 100644 --- a/app/react/portainer/access-control/AccessControlPanel/AccessControlPanelForm.tsx +++ b/app/react/portainer/access-control/AccessControlPanel/AccessControlPanelForm.tsx @@ -6,7 +6,7 @@ import { object } from 'yup'; import { useUser } from '@/portainer/hooks/useUser'; import { confirmAsync } from '@/portainer/services/modal.service/confirm'; import { notifySuccess } from '@/portainer/services/notifications'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Button } from '@@/buttons'; import { LoadingButton } from '@@/buttons/LoadingButton'; diff --git a/app/react/portainer/access-control/EditDetails/EditDetails.tsx b/app/react/portainer/access-control/EditDetails/EditDetails.tsx index 157f8e3f3..18fb4d3a1 100644 --- a/app/react/portainer/access-control/EditDetails/EditDetails.tsx +++ b/app/react/portainer/access-control/EditDetails/EditDetails.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { FormikErrors } from 'formik'; import { useUser } from '@/portainer/hooks/useUser'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { BoxSelector } from '@@/BoxSelector'; import { FormError } from '@@/form-components/FormError'; diff --git a/app/react/portainer/access-control/EditDetails/useLoadState.ts b/app/react/portainer/access-control/EditDetails/useLoadState.ts index ce44aa385..18904d7d9 100644 --- a/app/react/portainer/access-control/EditDetails/useLoadState.ts +++ b/app/react/portainer/access-control/EditDetails/useLoadState.ts @@ -1,6 +1,6 @@ import { useTeams } from '@/react/portainer/users/teams/queries'; import { useUsers } from '@/portainer/users/queries'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; export function useLoadState(environmentId?: EnvironmentId) { const teams = useTeams(false, environmentId); diff --git a/app/react/portainer/access-control/EditDetails/useOptions.tsx b/app/react/portainer/access-control/EditDetails/useOptions.tsx index 3f8289fdc..5a04c618a 100644 --- a/app/react/portainer/access-control/EditDetails/useOptions.tsx +++ b/app/react/portainer/access-control/EditDetails/useOptions.tsx @@ -6,7 +6,7 @@ import { ownershipIcon } from '@/portainer/filters/filters'; import { Team } from '@/react/portainer/users/teams/types'; import { BoxSelectorOption } from '@@/BoxSelector/types'; -import { BadgeIcon } from '@@/BoxSelector/BadgeIcon'; +import { BadgeIcon } from '@@/BadgeIcon'; import { ResourceControlOwnership } from '../types'; diff --git a/app/portainer/environments/environment.service/create.ts b/app/react/portainer/environments/environment.service/create.ts similarity index 100% rename from app/portainer/environments/environment.service/create.ts rename to app/react/portainer/environments/environment.service/create.ts diff --git a/app/portainer/environments/environment.service/edge.ts b/app/react/portainer/environments/environment.service/edge.ts similarity index 100% rename from app/portainer/environments/environment.service/edge.ts rename to app/react/portainer/environments/environment.service/edge.ts diff --git a/app/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts similarity index 100% rename from app/portainer/environments/environment.service/index.ts rename to app/react/portainer/environments/environment.service/index.ts diff --git a/app/portainer/environments/environment.service/registries.ts b/app/react/portainer/environments/environment.service/registries.ts similarity index 100% rename from app/portainer/environments/environment.service/registries.ts rename to app/react/portainer/environments/environment.service/registries.ts diff --git a/app/portainer/environments/environment.service/utils.ts b/app/react/portainer/environments/environment.service/utils.ts similarity index 100% rename from app/portainer/environments/environment.service/utils.ts rename to app/react/portainer/environments/environment.service/utils.ts diff --git a/app/portainer/environments/queries/useAgentDetails.ts b/app/react/portainer/environments/queries/useAgentDetails.ts similarity index 100% rename from app/portainer/environments/queries/useAgentDetails.ts rename to app/react/portainer/environments/queries/useAgentDetails.ts diff --git a/app/portainer/environments/queries/useAgentVersionsList.ts b/app/react/portainer/environments/queries/useAgentVersionsList.ts similarity index 100% rename from app/portainer/environments/queries/useAgentVersionsList.ts rename to app/react/portainer/environments/queries/useAgentVersionsList.ts diff --git a/app/portainer/environments/queries/useCreateEnvironmentMutation.ts b/app/react/portainer/environments/queries/useCreateEnvironmentMutation.ts similarity index 100% rename from app/portainer/environments/queries/useCreateEnvironmentMutation.ts rename to app/react/portainer/environments/queries/useCreateEnvironmentMutation.ts diff --git a/app/portainer/environments/queries/useEnvironment.ts b/app/react/portainer/environments/queries/useEnvironment.ts similarity index 68% rename from app/portainer/environments/queries/useEnvironment.ts rename to app/react/portainer/environments/queries/useEnvironment.ts index 80610ac2f..63f98e031 100644 --- a/app/portainer/environments/queries/useEnvironment.ts +++ b/app/react/portainer/environments/queries/useEnvironment.ts @@ -1,7 +1,7 @@ import { useQuery } from 'react-query'; -import { getEndpoint } from '@/portainer/environments/environment.service'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { getEndpoint } from '@/react/portainer/environments/environment.service'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { withError } from '@/react-tools/react-query'; export function useEnvironment(id?: EnvironmentId) { diff --git a/app/portainer/environments/queries/useEnvironmentList.ts b/app/react/portainer/environments/queries/useEnvironmentList.ts similarity index 100% rename from app/portainer/environments/queries/useEnvironmentList.ts rename to app/react/portainer/environments/queries/useEnvironmentList.ts diff --git a/app/portainer/environments/types.ts b/app/react/portainer/environments/types.ts similarity index 99% rename from app/portainer/environments/types.ts rename to app/react/portainer/environments/types.ts index 286022f82..44aa83292 100644 --- a/app/portainer/environments/types.ts +++ b/app/react/portainer/environments/types.ts @@ -70,6 +70,7 @@ export interface KubernetesConfiguration { RestrictDefaultNamespace?: boolean; IngressClasses: IngressClass[]; IngressAvailabilityPerNamespace: boolean; + AllowNoneIngressClass: boolean; } export interface KubernetesSettings { diff --git a/app/react/portainer/environments/update-schedules/common/ActiveSchedulesNotice.tsx b/app/react/portainer/environments/update-schedules/common/ActiveSchedulesNotice.tsx index 113394f65..62e2af464 100644 --- a/app/react/portainer/environments/update-schedules/common/ActiveSchedulesNotice.tsx +++ b/app/react/portainer/environments/update-schedules/common/ActiveSchedulesNotice.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import { Clock } from 'react-feather'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; diff --git a/app/react/portainer/environments/update-schedules/common/EnvironmentSelection.tsx b/app/react/portainer/environments/update-schedules/common/EnvironmentSelection.tsx index 9cd313048..e08cae128 100644 --- a/app/react/portainer/environments/update-schedules/common/EnvironmentSelection.tsx +++ b/app/react/portainer/environments/update-schedules/common/EnvironmentSelection.tsx @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { TextTip } from '@@/Tip/TextTip'; diff --git a/app/react/portainer/environments/update-schedules/common/EnvironmentSelectionItem.tsx b/app/react/portainer/environments/update-schedules/common/EnvironmentSelectionItem.tsx index 1573eda8d..0d80f1a28 100644 --- a/app/react/portainer/environments/update-schedules/common/EnvironmentSelectionItem.tsx +++ b/app/react/portainer/environments/update-schedules/common/EnvironmentSelectionItem.tsx @@ -2,7 +2,7 @@ import { useField } from 'formik'; import _ from 'lodash'; import { useState, ChangeEvent } from 'react'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { Select } from '@@/form-components/Input'; import { Checkbox } from '@@/form-components/Checkbox'; diff --git a/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx b/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx index e3581b6ab..72d760775 100644 --- a/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx +++ b/app/react/portainer/environments/update-schedules/common/UpdateScheduleDetailsFieldset.tsx @@ -1,8 +1,8 @@ import { useFormikContext } from 'formik'; import { useCurrentStateAndParams } from '@uirouter/react'; -import { EdgeTypes, EnvironmentId } from '@/portainer/environments/types'; -import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList'; +import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types'; +import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList'; import { useActiveSchedules } from '../queries/useActiveSchedules'; diff --git a/app/react/portainer/environments/update-schedules/common/types.ts b/app/react/portainer/environments/update-schedules/common/types.ts index 8cff1e558..30a8e9422 100644 --- a/app/react/portainer/environments/update-schedules/common/types.ts +++ b/app/react/portainer/environments/update-schedules/common/types.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; import { ScheduleType } from '../types'; diff --git a/app/react/portainer/environments/update-schedules/queries/query-keys.ts b/app/react/portainer/environments/update-schedules/queries/query-keys.ts index 6e6628660..345b2bc87 100644 --- a/app/react/portainer/environments/update-schedules/queries/query-keys.ts +++ b/app/react/portainer/environments/update-schedules/queries/query-keys.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { EdgeUpdateSchedule } from '../types'; diff --git a/app/react/portainer/environments/update-schedules/queries/useActiveSchedules.ts b/app/react/portainer/environments/update-schedules/queries/useActiveSchedules.ts index e5189860b..5e781f129 100644 --- a/app/react/portainer/environments/update-schedules/queries/useActiveSchedules.ts +++ b/app/react/portainer/environments/update-schedules/queries/useActiveSchedules.ts @@ -1,7 +1,7 @@ import { useQuery } from 'react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { EdgeUpdateSchedule, ScheduleType, StatusType } from '../types'; diff --git a/app/react/portainer/environments/update-schedules/queries/usePreviousVersions.ts b/app/react/portainer/environments/update-schedules/queries/usePreviousVersions.ts index d3c237e30..87029d749 100644 --- a/app/react/portainer/environments/update-schedules/queries/usePreviousVersions.ts +++ b/app/react/portainer/environments/update-schedules/queries/usePreviousVersions.ts @@ -1,7 +1,7 @@ import { useQuery } from 'react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { queryKeys } from './query-keys'; import { buildUrl } from './urls'; diff --git a/app/react/portainer/environments/update-schedules/types.ts b/app/react/portainer/environments/update-schedules/types.ts index d78a076e5..3d1606dc4 100644 --- a/app/react/portainer/environments/update-schedules/types.ts +++ b/app/react/portainer/environments/update-schedules/types.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { UserId } from '@/portainer/users/types'; import { EdgeGroup } from '@/react/edge/edge-groups/types'; diff --git a/app/react/portainer/environments/utils/get-platform-icon.ts b/app/react/portainer/environments/utils/get-platform-icon.ts index f4c3227b3..c26231a05 100644 --- a/app/react/portainer/environments/utils/get-platform-icon.ts +++ b/app/react/portainer/environments/utils/get-platform-icon.ts @@ -1,5 +1,8 @@ -import { getPlatformType } from '@/portainer/environments/utils'; -import { EnvironmentType, PlatformType } from '@/portainer/environments/types'; +import { getPlatformType } from '@/react/portainer/environments/utils'; +import { + EnvironmentType, + PlatformType, +} from '@/react/portainer/environments/types'; import Docker from './docker.svg?c'; import Azure from './azure.svg?c'; diff --git a/app/portainer/environments/utils.ts b/app/react/portainer/environments/utils/index.ts similarity index 96% rename from app/portainer/environments/utils.ts rename to app/react/portainer/environments/utils/index.ts index 66bc2afe5..97d967bd6 100644 --- a/app/portainer/environments/utils.ts +++ b/app/react/portainer/environments/utils/index.ts @@ -1,4 +1,4 @@ -import { Environment, EnvironmentType, PlatformType } from './types'; +import { Environment, EnvironmentType, PlatformType } from '../types'; export function getPlatformType(envType: EnvironmentType) { switch (envType) { diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx index a7c55504a..069c4b11d 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx @@ -4,7 +4,10 @@ import _ from 'lodash'; import clsx from 'clsx'; import { notifyError } from '@/portainer/services/notifications'; -import { Environment, EnvironmentId } from '@/portainer/environments/types'; +import { + Environment, + EnvironmentId, +} from '@/react/portainer/environments/types'; import { useAnalytics } from '@/angulartics.matomo/analytics-services'; import { Stepper } from '@@/Stepper'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx index 16cba1141..d2922ac9c 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardAzure/WizardAzure.tsx @@ -3,10 +3,10 @@ import { useReducer, useState } from 'react'; import { object, SchemaOf, string } from 'yup'; import { buildOption } from '@/portainer/components/BoxSelector'; -import { useCreateAzureEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation'; +import { useCreateAzureEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Environment } from '@/portainer/environments/types'; -import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create'; +import { Environment } from '@/react/portainer/environments/types'; +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { Input } from '@@/form-components/Input'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx index f595856cc..66a42d1c0 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APIForm.tsx @@ -1,13 +1,13 @@ import { Field, Form, Formik } from 'formik'; import { useReducer } from 'react'; -import { useCreateRemoteEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation'; +import { useCreateRemoteEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware'; import { notifySuccess } from '@/portainer/services/notifications'; import { Environment, EnvironmentCreationTypes, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { FormControl } from '@@/form-components/FormControl'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx index ee50dcd4b..3b39cd44d 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/APITab.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { APIForm } from './APIForm'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx index 39976ecbc..5da59bcf4 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails'; +import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; import { CopyButton } from '@@/buttons/CopyButton'; import { Code } from '@@/Code'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts index 0cbdca70f..4112267d5 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/types.ts @@ -1,5 +1,5 @@ import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; -import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create'; +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { name: string; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx index c3747fa08..7e9eac9d0 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/AgentTab.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { AgentForm } from '../../shared/AgentForm/AgentForm'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/DeploymentScripts.tsx index ac817d1b6..edb987467 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/DeploymentScripts.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/AgentTab/DeploymentScripts.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails'; +import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; import { CopyButton } from '@@/buttons/CopyButton'; import { Code } from '@@/Code'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx index 832fb1e09..6088717bd 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketForm.tsx @@ -1,10 +1,10 @@ import { Field, Form, Formik, useFormikContext } from 'formik'; import { useReducer } from 'react'; -import { useCreateLocalDockerEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation'; +import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { FormControl } from '@@/form-components/FormControl'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx index d4178c39c..a93b0e253 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/SocketTab.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { DeploymentScripts } from '../APITab/DeploymentScripts'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts index ab9cf9b6a..84622bd39 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/SocketTab/types.ts @@ -1,5 +1,5 @@ import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; -import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create'; +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { name: string; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx index 3fec1dda0..045c0e692 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx index f77c68544..60d5c57bb 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardEndpointsList/WizardEndpointsList.tsx @@ -5,16 +5,16 @@ import { endpointTypeName, stripProtocol, } from '@/portainer/filters/filters'; -import { EnvironmentId } from '@/portainer/environments/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; import { EdgeIndicator } from '@/portainer/home/EnvironmentList/EnvironmentItem'; import { isEdgeEnvironment, isUnassociatedEdgeEnvironment, -} from '@/portainer/environments/utils'; +} from '@/react/portainer/environments/utils'; import { ENVIRONMENTS_POLLING_INTERVAL, useEnvironmentList, -} from '@/portainer/environments/queries/useEnvironmentList'; +} from '@/react/portainer/environments/queries/useEnvironmentList'; import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx index dec3b9f56..24d9b6717 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/AgentPanel.tsx @@ -1,4 +1,4 @@ -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { AgentForm } from '../shared/AgentForm'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx index 6c88fa98f..82c8d9298 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/DeploymentScripts.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers'; -import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails'; +import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; import { CopyButton } from '@@/buttons/CopyButton'; import { Code } from '@@/Code'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx index 5f18aefa5..44c0b99df 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { Environment, EnvironmentCreationTypes, -} from '@/portainer/environments/types'; +} from '@/react/portainer/environments/types'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; import { FeatureId } from '@/portainer/feature-flags/enums'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx index b20b6dab7..5a9f5ff33 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.tsx @@ -1,10 +1,10 @@ import { Form, Formik } from 'formik'; import { useReducer } from 'react'; -import { useCreateAgentEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation'; +import { useCreateAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { notifySuccess } from '@/portainer/services/notifications'; -import { Environment } from '@/portainer/environments/types'; -import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create'; +import { Environment } from '@/react/portainer/environments/types'; +import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { Icon } from '@@/Icon'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx index 41952e930..61ff688f9 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/AgentForm/AgentForm.validation.tsx @@ -1,7 +1,7 @@ import { object, SchemaOf, string } from 'yup'; import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; -import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create'; +import { CreateAgentEnvironmentValues } from '@/react/portainer/environments/environment.service/create'; import { metadataValidation } from '../MetadataFieldset/validation'; import { nameValidation } from '../NameField'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx index df8569e91..92dab95c5 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx @@ -1,7 +1,7 @@ import { Formik, Form } from 'formik'; -import { Environment } from '@/portainer/environments/types'; -import { useCreateEdgeAgentEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation'; +import { Environment } from '@/react/portainer/environments/types'; +import { useCreateEdgeAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; import { baseHref } from '@/portainer/helpers/pathHelper'; import { EdgeCheckinIntervalField } from '@/edge/components/EdgeCheckInIntervalField'; import { useCreateEdgeDeviceParam } from '@/react/portainer/environments/wizard/hooks/useCreateEdgeDeviceParam'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts index 3411eb23a..bdf05b59a 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts @@ -1,5 +1,5 @@ import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; -import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create'; +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export interface FormValues { name: string; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx index 8dbafa540..118efb107 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx @@ -1,7 +1,7 @@ import { v4 as uuid } from 'uuid'; import { useReducer, useState } from 'react'; -import { Environment } from '@/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm'; import { CommandTab } from '@/react/edge/components/EdgeScriptForm/scripts'; import { OS, EdgeInfo } from '@/react/edge/components/EdgeScriptForm/types'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts index 2b447061b..f119c1389 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/validation.ts @@ -1,6 +1,6 @@ import { object, number, array, SchemaOf } from 'yup'; -import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create'; +import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; export function metadataValidation(): SchemaOf { return object({ diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx index 78fa82e36..ada0b4683 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/NameField.tsx @@ -2,7 +2,7 @@ import { Field, useField } from 'formik'; import { string } from 'yup'; import { debounce } from 'lodash'; -import { getEnvironments } from '@/portainer/environments/environment.service'; +import { getEnvironments } from '@/react/portainer/environments/environment.service'; import { FormControl } from '@@/form-components/FormControl'; import { Input } from '@@/form-components/Input'; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/useOnlyEdgeOptions.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/useOnlyEdgeOptions.ts index e14890c1f..89ded5627 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/useOnlyEdgeOptions.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/useOnlyEdgeOptions.ts @@ -1,4 +1,4 @@ -import { EnvironmentCreationTypes } from '@/portainer/environments/types'; +import { EnvironmentCreationTypes } from '@/react/portainer/environments/types'; import { BoxSelectorOption } from '@@/BoxSelector'; diff --git a/app/react/portainer/environments/wizard/HomeView/HomeView.tsx b/app/react/portainer/environments/wizard/HomeView/HomeView.tsx index 23f61eb0f..bf5d1d2ce 100644 --- a/app/react/portainer/environments/wizard/HomeView/HomeView.tsx +++ b/app/react/portainer/environments/wizard/HomeView/HomeView.tsx @@ -1,4 +1,4 @@ -import { EnvironmentType } from '@/portainer/environments/types'; +import { EnvironmentType } from '@/react/portainer/environments/types'; import { useAnalytics } from '@/angulartics.matomo/analytics-services'; import { PageHeader } from '@@/PageHeader'; diff --git a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts index 6088a2f94..72faf893d 100644 --- a/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts +++ b/app/react/portainer/environments/wizard/HomeView/useFetchOrCreateLocalEnvironment.ts @@ -1,12 +1,15 @@ import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; -import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList'; -import { Environment, EnvironmentType } from '@/portainer/environments/types'; +import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList'; +import { + Environment, + EnvironmentType, +} from '@/react/portainer/environments/types'; import { createLocalDockerEnvironment, createLocalKubernetesEnvironment, -} from '@/portainer/environments/environment.service/create'; +} from '@/react/portainer/environments/environment.service/create'; export function useConnectLocalEnvironment(): { status: 'error' | 'loading' | 'success'; diff --git a/app/react/portainer/notifications/NotificationsView.tsx b/app/react/portainer/notifications/NotificationsView.tsx index 3b4069500..90dca55b0 100644 --- a/app/react/portainer/notifications/NotificationsView.tsx +++ b/app/react/portainer/notifications/NotificationsView.tsx @@ -18,7 +18,7 @@ import { columns } from './columns'; import { createStore } from './datatable-store'; const storageKey = 'notifications-list'; -const useSettingsStore = createStore(storageKey, 'time'); +const useSettingsStore = createStore(storageKey, 'time', true); export function NotificationsView() { const settingsStore = useSettingsStore(); diff --git a/app/react/portainer/notifications/datatable-store.ts b/app/react/portainer/notifications/datatable-store.ts index acc4e5770..d0e0e4c81 100644 --- a/app/react/portainer/notifications/datatable-store.ts +++ b/app/react/portainer/notifications/datatable-store.ts @@ -19,11 +19,15 @@ interface TableSettings SettableColumnsTableSettings, RefreshableTableSettings {} -export function createStore(storageKey: string, initialSortBy?: string) { +export function createStore( + storageKey: string, + initialSortBy?: string, + desc?: boolean +) { return create()( persist( (set) => ({ - ...sortableSettings(set, initialSortBy), + ...sortableSettings(set, initialSortBy, desc), ...paginationSettings(set), ...hiddenColumnsSettings(set), ...refreshableSettings(set), diff --git a/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx b/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx index 3ccc52d59..8f76b49ee 100644 --- a/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx +++ b/app/react/portainer/registries/ListView/DefaultRegistry/DefaultRegistryAction.tsx @@ -1,3 +1,5 @@ +import { Eye, EyeOff } from 'react-feather'; + import { notifySuccess } from '@/portainer/services/notifications'; import { FeatureId } from '@/portainer/feature-flags/enums'; import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service'; @@ -8,7 +10,6 @@ import { import { Tooltip } from '@@/Tip/Tooltip'; import { Button } from '@@/buttons'; -import { Icon } from '@@/Icon'; import { BEFeatureIndicator } from '@@/BEFeatureIndicator'; export function DefaultRegistryAction() { @@ -29,11 +30,11 @@ export function DefaultRegistryAction() { {!hideDefaultRegistry ? (
@@ -46,11 +47,7 @@ export function DefaultRegistryAction() {
) : (
- state + 1, 0); + const teamSyncQuery = usePublicSettings({ + select: (settings) => settings.TeamSync, + }); const initialValues = { name: '', @@ -95,10 +100,22 @@ export function CreateTeamForm({ users, teams }: Props) { dataCy="team-teamLeaderSelect" inputId="users-input" placeholder="Select one or more team leaders" + disabled={teamSyncQuery.data} /> )} + {teamSyncQuery.data && ( +
+
+ + The team leader feature is disabled as external + authentication is currently enabled with team sync. + +
+
+ )} +