Compare commits
15 Commits
develop
...
fix/CE/466
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3f7212adc | ||
|
|
60d889e043 | ||
|
|
3bb825c10c | ||
|
|
5bd7f8b636 | ||
|
|
88f0b66cdf | ||
|
|
5b4696dd6c | ||
|
|
c8318d55b1 | ||
|
|
2ff7ebebf5 | ||
|
|
634ee9da72 | ||
|
|
5cbdb0b164 | ||
|
|
3821370fc2 | ||
|
|
e850aee966 | ||
|
|
18842045fc | ||
|
|
846d7e633b | ||
|
|
f443dd113d |
40
api/bolt/migrator/migrate_dbversion26.go
Normal file
40
api/bolt/migrator/migrate_dbversion26.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
resourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resource := range resourceControls {
|
||||
if resource.Type != portainer.StackResourceControl {
|
||||
continue
|
||||
}
|
||||
|
||||
stackName := resource.ResourceID
|
||||
|
||||
stack, err := m.stackService.StackByName(stackName)
|
||||
if err != nil {
|
||||
if err == errors.ErrObjectNotFound {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
|
||||
|
||||
err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package migrator
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/endpoint"
|
||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
@@ -350,5 +350,13 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ func initCLI() *portainer.CLIFlags {
|
||||
var cliService portainer.CLIService = &cli.Service{}
|
||||
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed parsing flags: %v", err)
|
||||
}
|
||||
|
||||
err = cliService.ValidateFlags(flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed validating flags:%v", err)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func initCLI() *portainer.CLIFlags {
|
||||
func initFileService(dataStorePath string) portainer.FileService {
|
||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed creating file service: %v", err)
|
||||
}
|
||||
return fileService
|
||||
}
|
||||
@@ -54,22 +54,22 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed migration: %v", err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
|
||||
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
existingKeyPair, err := fileService.KeyPairFilesExist()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed checking for existing key pair: %v", err)
|
||||
}
|
||||
|
||||
if existingKeyPair {
|
||||
@@ -359,7 +359,7 @@ func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
|
||||
|
||||
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed getting admin user: %v", err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
@@ -382,7 +382,7 @@ func main() {
|
||||
|
||||
jwtService, err := initJWTService(dataStore)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing JWT service: %v", err)
|
||||
}
|
||||
|
||||
ldapService := initLDAPService()
|
||||
@@ -397,14 +397,14 @@ func main() {
|
||||
|
||||
err = initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing key pai: %v", err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed getting instance id: %v", err)
|
||||
}
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
@@ -412,13 +412,13 @@ func main() {
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing snapshot service: %v", err)
|
||||
}
|
||||
snapshotService.Start()
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing swarm stack manager: %v", err)
|
||||
}
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
@@ -430,31 +430,31 @@ func main() {
|
||||
if dataStore.IsNew() {
|
||||
err = updateSettingsFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed updating settings from flags: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed loading edge jobs from database: %v", err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(flags)
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed initializing endpoint: %v", err)
|
||||
}
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed getting admin password file: %v", err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed hashing admin password: %v", err)
|
||||
}
|
||||
} else if *flags.AdminPassword != "" {
|
||||
adminPasswordHash = *flags.AdminPassword
|
||||
@@ -463,7 +463,7 @@ func main() {
|
||||
if adminPasswordHash != "" {
|
||||
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed getting admin user: %v", err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
@@ -475,7 +475,7 @@ func main() {
|
||||
}
|
||||
err := dataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed creating admin user: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
|
||||
@@ -486,7 +486,7 @@ func main() {
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed starting tunnel server: %v", err)
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
@@ -518,6 +518,6 @@ func main() {
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed starting server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
18
api/go.mod
18
api/go.mod
@@ -4,7 +4,6 @@ go 1.13
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.14
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
@@ -15,35 +14,26 @@ require (
|
||||
github.com/docker/docker v0.0.0-00010101000000-000000000000
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/gofrs/uuid v3.3.0+incompatible
|
||||
github.com/golang/protobuf v1.3.3 // indirect
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/imdario/mergo v0.3.8 // indirect
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/json-iterator/go v1.1.8
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/portainer/libcompose v0.5.3
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d // indirect
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
|
||||
49
api/go.sum
49
api/go.sum
@@ -24,8 +24,6 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBb
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
|
||||
@@ -50,7 +48,6 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -106,8 +103,8 @@ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
|
||||
@@ -121,8 +118,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -171,8 +166,6 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
@@ -191,8 +184,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
@@ -214,8 +205,6 @@ github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -282,7 +271,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE=
|
||||
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
@@ -300,8 +288,8 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -319,10 +307,11 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
@@ -331,9 +320,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -346,19 +334,15 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -376,6 +360,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
|
||||
@@ -386,9 +371,8 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
@@ -402,13 +386,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -100,11 +101,13 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
||||
@@ -2,6 +2,7 @@ package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
@@ -51,15 +52,13 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -150,15 +149,13 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -249,15 +246,13 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
|
||||
@@ -2,10 +2,10 @@ package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
@@ -47,15 +47,13 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -150,15 +148,13 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -257,15 +253,13 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, payload.Name) {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/mux"
|
||||
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"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -24,6 +28,7 @@ type Handler struct {
|
||||
requestBouncer *security.RequestBouncer
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
@@ -103,3 +108,50 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
|
||||
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, name) && (stackID == 0 || stackID != stack.ID) && stack.EndpointID == endpoint.ID {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
if swarmMode {
|
||||
services, err := dockerClient.ServiceList(context.Background(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
serviceNS, ok := service.Spec.Labels["com.docker.stack.namespace"]
|
||||
if ok && serviceNS == name {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
containerNS, ok := container.Labels["com.docker.compose.project"]
|
||||
|
||||
if ok && containerNS == name {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
||||
@@ -208,9 +209,9 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
} else {
|
||||
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID)
|
||||
}
|
||||
|
||||
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// @id StackDelete
|
||||
@@ -82,7 +83,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
type stackFileResponse struct {
|
||||
@@ -57,7 +58,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// @id StackInspect
|
||||
@@ -56,7 +57,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
type stackMigratePayload struct {
|
||||
@@ -76,7 +78,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
@@ -122,6 +124,16 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
stack.Name = payload.Name
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
||||
if migrationError != nil {
|
||||
return migrationError
|
||||
|
||||
@@ -2,11 +2,13 @@ package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -57,7 +59,16 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
if !isUnique {
|
||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
|
||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// @id StackStop
|
||||
@@ -56,7 +57,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
type updateComposeStackPayload struct {
|
||||
@@ -99,7 +100,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -117,17 +119,17 @@ func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resour
|
||||
|
||||
switch resourceType {
|
||||
case portainer.ContainerResourceControl:
|
||||
return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromContainerLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
case portainer.NetworkResourceControl:
|
||||
return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromNetworkLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
case portainer.VolumeResourceControl:
|
||||
return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromVolumeLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
case portainer.ServiceResourceControl:
|
||||
return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromServiceLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
case portainer.ConfigResourceControl:
|
||||
return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromConfigLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
case portainer.SecretResourceControl:
|
||||
return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls)
|
||||
return getInheritedResourceControlFromSecretLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -273,8 +275,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
|
||||
}
|
||||
|
||||
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
|
||||
inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
|
||||
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
|
||||
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
|
||||
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
|
||||
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
@@ -282,8 +285,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
|
||||
}
|
||||
|
||||
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
|
||||
inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
|
||||
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
|
||||
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
|
||||
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
|
||||
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
@@ -296,6 +300,20 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string {
|
||||
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != "" {
|
||||
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName]
|
||||
return stackutils.ResourceControlID(endpointID, stackName)
|
||||
}
|
||||
|
||||
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != "" {
|
||||
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName]
|
||||
return stackutils.ResourceControlID(endpointID, stackName)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||
if object["Portainer"] == nil {
|
||||
object["Portainer"] = make(map[string]interface{})
|
||||
|
||||
@@ -15,15 +15,15 @@ const (
|
||||
configObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, endpointID portainer.EndpointID, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(config.Spec.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
containerObjectIdentifier = "Id"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -33,14 +33,9 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
|
||||
}
|
||||
}
|
||||
|
||||
swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
|
||||
if composeStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(container.Config.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -18,15 +19,15 @@ const (
|
||||
networkObjectName = "Name"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, endpointID portainer.EndpointID, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(network.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -15,15 +15,15 @@ const (
|
||||
secretObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, endpointID portainer.EndpointID, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(secret.Spec.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -20,15 +20,15 @@ const (
|
||||
serviceObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(service.Spec.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
@@ -18,15 +18,15 @@ const (
|
||||
volumeObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, endpointID portainer.EndpointID, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
volume, err := dockerClient.VolumeInspect(context.Background(), volumeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
stackResourceID := getStackResourceIDFromLabels(volume.Labels, endpointID)
|
||||
if stackResourceID != "" {
|
||||
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -157,6 +157,7 @@ func (server *Server) Start() error {
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.DataStore = server.DataStore
|
||||
stackHandler.DockerClientFactory = server.DockerClientFactory
|
||||
stackHandler.FileService = server.FileService
|
||||
stackHandler.SwarmStackManager = server.SwarmStackManager
|
||||
stackHandler.ComposeStackManager = server.ComposeStackManager
|
||||
|
||||
@@ -3,7 +3,8 @@ package authorization
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
|
||||
@@ -110,7 +111,7 @@ func NewRestrictedResourceControl(resourceIdentifier string, resourceType portai
|
||||
func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack {
|
||||
for idx, stack := range stacks {
|
||||
|
||||
resourceControl := GetResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl, resourceControls)
|
||||
resourceControl := GetResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, resourceControls)
|
||||
if resourceControl != nil {
|
||||
stacks[idx].ResourceControl = resourceControl
|
||||
}
|
||||
|
||||
12
api/internal/stackutils/stackutils.go
Normal file
12
api/internal/stackutils/stackutils.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package stackutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// ResourceControlID returns the stack resource control id
|
||||
func ResourceControlID(endpointID portainer.EndpointID, name string) string {
|
||||
return fmt.Sprintf("%d_%s", endpointID, name)
|
||||
}
|
||||
@@ -249,6 +249,8 @@ type (
|
||||
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion" example:"3.8"`
|
||||
// Endpoint specific security settings
|
||||
SecuritySettings EndpointSecuritySettings
|
||||
// LastCheckInDate mark last check-in date on checkin
|
||||
LastCheckInDate int64
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
@@ -339,7 +341,7 @@ type (
|
||||
// EndpointType represents the type of an endpoint
|
||||
EndpointType int
|
||||
|
||||
// EndpointRelation represnts a endpoint relation object
|
||||
// EndpointRelation represents a endpoint relation object
|
||||
EndpointRelation struct {
|
||||
EndpointID EndpointID
|
||||
EdgeStacks map[EdgeStackID]bool
|
||||
@@ -1182,7 +1184,7 @@ type (
|
||||
DeleteResourceControl(ID ResourceControlID) error
|
||||
}
|
||||
|
||||
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
||||
// ReverseTunnelService represents a service used to manage reverse tunnel connections.
|
||||
ReverseTunnelService interface {
|
||||
StartTunnelServer(addr, port string, snapshotService SnapshotService) error
|
||||
GenerateEdgeKey(url, host string, endpointIdentifier int) string
|
||||
@@ -1224,7 +1226,7 @@ type (
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
// StackService represents a service for managing endpoint snapshots
|
||||
// SnapshotService represents a service for managing endpoint snapshots
|
||||
SnapshotService interface {
|
||||
Start()
|
||||
SetSnapshotInterval(snapshotInterval string) error
|
||||
@@ -1312,7 +1314,7 @@ const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.2.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 26
|
||||
DBVersion = 27
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
@@ -1547,6 +1549,7 @@ const (
|
||||
EdgeAgentActive string = "ACTIVE"
|
||||
)
|
||||
|
||||
// represents an authorization type
|
||||
const (
|
||||
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
||||
OperationDockerContainerList Authorization = "DockerContainerList"
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
Container capabilities
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div ng-repeat="cap in $ctrl.capabilities">
|
||||
<div class="col-xs-8 col-sm-3 col-md-2">
|
||||
<label for="capability" class="control-label text-left">
|
||||
{{ cap.capability }}
|
||||
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
|
||||
</label>
|
||||
<div ng-repeat="cap in $ctrl.capabilities" class="col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<label for="capability" class="control-label text-left" style="display: flex; padding: 0;">
|
||||
{{ cap.capability }}
|
||||
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<label class="switch"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4 col-sm-2 col-md-1">
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
|
||||
</div>
|
||||
<div class="col-xs-0 col-sm-1 col-md-1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -176,6 +176,13 @@
|
||||
Created
|
||||
</a>
|
||||
</th>
|
||||
<th ng-show="$ctrl.columnVisibility.columns.ip.display">
|
||||
<a ng-click="$ctrl.changeOrderBy('IP')">
|
||||
IP Address
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">
|
||||
<a ng-click="$ctrl.changeOrderBy('NodeName')">
|
||||
Host
|
||||
@@ -250,6 +257,7 @@
|
||||
<td ng-show="$ctrl.columnVisibility.columns.created.display">
|
||||
{{ item.Created | getisodatefromtimestamp }}
|
||||
</td>
|
||||
<td ng-show="$ctrl.columnVisibility.columns.ip.display">{{ item.IP ? item.IP : '-' }}</td>
|
||||
<td ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">{{ item.NodeName ? item.NodeName : '-' }}</td>
|
||||
<td ng-show="$ctrl.columnVisibility.columns.ports.display">
|
||||
<a
|
||||
|
||||
@@ -57,6 +57,10 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
label: 'Created',
|
||||
display: true,
|
||||
},
|
||||
ip: {
|
||||
label: 'IP Address',
|
||||
display: true,
|
||||
},
|
||||
host: {
|
||||
label: 'Host',
|
||||
display: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655
|
||||
|
||||
function parsePort(port) {
|
||||
if (portPattern.test(port)) {
|
||||
return parseInt(port);
|
||||
return parseInt(port, 10);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
@@ -211,14 +211,14 @@ angular.module('portainer.docker').factory('ContainerHelper', [
|
||||
_.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => {
|
||||
// Sort by host port
|
||||
const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => {
|
||||
return parseInt(_.split(portKey, '/')[0]);
|
||||
return parseInt(_.split(portKey, '/')[0], 10);
|
||||
});
|
||||
|
||||
let previousHostPort = -1;
|
||||
let previousContainerPort = -1;
|
||||
_.forEach(sortedPortBindingKeys, (portKey) => {
|
||||
const portKeySplit = _.split(portKey, '/');
|
||||
const containerPort = parseInt(portKeySplit[0]);
|
||||
const containerPort = parseInt(portKeySplit[0], 10);
|
||||
const portBinding = portBindings[portKey][0];
|
||||
portBindings[portKey].shift();
|
||||
const hostPort = parsePort(portBinding.HostPort);
|
||||
|
||||
@@ -31,7 +31,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
'HttpRequestHelper',
|
||||
'NodeService',
|
||||
'WebhookService',
|
||||
'EndpointProvider',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
@@ -57,9 +56,10 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
HttpRequestHelper,
|
||||
NodeService,
|
||||
WebhookService,
|
||||
EndpointProvider,
|
||||
endpoint
|
||||
) {
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
@@ -493,7 +493,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
const resourceControl = data.Portainer.ResourceControl;
|
||||
const userId = Authentication.getUserDetails().ID;
|
||||
const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID()));
|
||||
const webhookPromise = $q.when(endpoint.Type !== 4 && $scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, endpoint.ID));
|
||||
return $q.all([rcPromise, webhookPromise]);
|
||||
})
|
||||
.then(function success() {
|
||||
|
||||
@@ -95,19 +95,21 @@
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- create-webhook -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Webhooks
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Create a service webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
|
||||
<div ng-if="endpoint.Type !== 4">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Webhooks
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Create a service webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !create-webhook -->
|
||||
|
||||
@@ -70,9 +70,12 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
|
||||
function prepareNFSConfiguration(driverOptions) {
|
||||
var data = $scope.formValues.NFSData;
|
||||
|
||||
driverOptions.push({ name: 'type', value: data.version.toLowerCase() });
|
||||
driverOptions.push({ name: 'type', value: 'nfs' });
|
||||
|
||||
var options = 'addr=' + data.serverAddress + ',' + data.options;
|
||||
if (data.version === 'NFS4') {
|
||||
options = options + ',nfsvers=4';
|
||||
}
|
||||
driverOptions.push({ name: 'o', value: options });
|
||||
|
||||
var mountPoint = data.mountPoint[0] === ':' ? data.mountPoint : ':' + data.mountPoint;
|
||||
|
||||
@@ -53,12 +53,17 @@
|
||||
<div
|
||||
class="col-sm-11 small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || $ctrl.state.duplicateKeys[index] !== undefined"
|
||||
ng-show="
|
||||
kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || $ctrl.state.duplicateKeys[index] !== undefined || $ctrl.state.invalidKeys[index]
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="$ctrl.state.duplicateKeys[index] !== undefined"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key is already defined.</p>
|
||||
<p ng-if="$ctrl.state.invalidKeys[index]"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key is invalid. A valid key must consist of alphanumeric characters, '-', '_' or '.'</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ class KubernetesConfigurationDataController {
|
||||
}
|
||||
|
||||
this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key));
|
||||
this.isValid = Object.keys(this.state.duplicateKeys).length === 0;
|
||||
this.state.invalidKeys = KubernetesFormValidationHelper.getInvalidKeys(_.map(this.formValues.Data, (data) => data.Key));
|
||||
this.isValid = Object.keys(this.state.duplicateKeys).length === 0 && Object.keys(this.state.invalidKeys).length === 0;
|
||||
}
|
||||
|
||||
addEntry() {
|
||||
@@ -94,6 +95,7 @@ class KubernetesConfigurationDataController {
|
||||
$onInit() {
|
||||
this.state = {
|
||||
duplicateKeys: {},
|
||||
invalidKeys: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<code-editor identifier="application-details-yaml" read-only="true" value="$ctrl.data"></code-editor>
|
||||
<div style="margin: 15px;">
|
||||
<span class="btn btn-primary btn-sm" ng-click="$ctrl.copyYAML()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy to clipboard</span>
|
||||
<span class="btn btn-primary btn-sm space-left" ng-click="$ctrl.toggleYAMLInspectorExpansion()">
|
||||
<i class="fa fa-{{ $ctrl.expanded ? 'minus' : 'plus' }} space-right" aria-hidden="true"></i>{{ $ctrl.expanded ? 'Collapse' : 'Expand' }}</span>
|
||||
<span id="copyNotificationYAML" style="margin-left: 7px; display: none; color: #23ae89;" class="small"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,20 @@ class KubernetesYamlInspectorController {
|
||||
|
||||
constructor(clipboard) {
|
||||
this.clipboard = clipboard;
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
copyYAML() {
|
||||
this.clipboard.copyText(this.data);
|
||||
$('#copyNotificationYAML').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
toggleYAMLInspectorExpansion() {
|
||||
let selector = 'kubernetes-yaml-inspector code-editor > div.CodeMirror';
|
||||
let height = this.expanded ? '500px' : '80vh';
|
||||
$(selector).css({ height: height });
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesYamlInspectorController;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
|
||||
import {
|
||||
|
||||
@@ -12,7 +12,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||
res.Name = data.metadata.name;
|
||||
res.Namespace = data.metadata.namespace;
|
||||
res.CreationDate = data.metadata.creationTimestamp;
|
||||
res.Storage = data.spec.resources.requests.storage.replace('i', 'B');
|
||||
res.Storage = `${data.spec.resources.requests.storage}B`;
|
||||
res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
|
||||
res.Yaml = yaml ? yaml.data : '';
|
||||
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
|
||||
@@ -35,7 +35,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
||||
}
|
||||
pvc.StorageClass = existantPVC.StorageClass;
|
||||
pvc.Storage = existantPVC.Storage.charAt(0) + 'i';
|
||||
pvc.Storage = existantPVC.Storage.charAt(0);
|
||||
pvc.CreationDate = existantPVC.CreationDate;
|
||||
pvc.Id = existantPVC.Id;
|
||||
} else {
|
||||
@@ -45,7 +45,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||
} else {
|
||||
pvc.Name = formValues.Name + '-' + pvc.Name;
|
||||
}
|
||||
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i';
|
||||
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0);
|
||||
pvc.StorageClass = item.StorageClass;
|
||||
}
|
||||
pvc.MountPath = item.ContainerPath;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { KubernetesResourcePool } from 'Kubernetes/models/resource-pool/models';
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesResourceQuotaConverter from './resourceQuota';
|
||||
|
||||
class KubernetesResourcePoolConverter {
|
||||
static apiToResourcePool(namespace) {
|
||||
@@ -7,6 +12,24 @@ class KubernetesResourcePoolConverter {
|
||||
res.Yaml = namespace.Yaml;
|
||||
return res;
|
||||
}
|
||||
|
||||
static formValuesToResourcePool(formValues) {
|
||||
const namespace = new KubernetesNamespace();
|
||||
namespace.Name = formValues.Name;
|
||||
namespace.ResourcePoolName = formValues.Name;
|
||||
namespace.ResourcePoolOwner = formValues.Owner;
|
||||
|
||||
const quota = KubernetesResourceQuotaConverter.resourcePoolFormValuesToResourceQuota(formValues);
|
||||
|
||||
const ingMap = _.map(formValues.IngressClasses, (c) => {
|
||||
if (c.Selected) {
|
||||
c.Namespace = namespace.Name;
|
||||
return KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
}
|
||||
});
|
||||
const ingresses = _.without(ingMap, undefined);
|
||||
return [namespace, quota, ingresses];
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolConverter;
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
|
||||
import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
|
||||
import { KubernetesResourceQuotaCreatePayload, KubernetesResourceQuotaUpdatePayload } from 'Kubernetes/models/resource-quota/payloads';
|
||||
import {
|
||||
KubernetesResourceQuota,
|
||||
KubernetesPortainerResourceQuotaCPULimit,
|
||||
KubernetesPortainerResourceQuotaMemoryLimit,
|
||||
KubernetesPortainerResourceQuotaCPURequest,
|
||||
KubernetesPortainerResourceQuotaMemoryRequest,
|
||||
KubernetesResourceQuotaDefaults,
|
||||
} from 'Kubernetes/models/resource-quota/models';
|
||||
import { KubernetesResourceQuotaCreatePayload } from 'Kubernetes/models/resource-quota/payloads';
|
||||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
|
||||
|
||||
class KubernetesResourceQuotaConverter {
|
||||
static apiToResourceQuota(data, yaml) {
|
||||
@@ -14,21 +24,21 @@ class KubernetesResourceQuotaConverter {
|
||||
res.Name = data.metadata.name;
|
||||
res.CpuLimit = 0;
|
||||
res.MemoryLimit = 0;
|
||||
if (data.spec.hard && data.spec.hard['limits.cpu']) {
|
||||
res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard['limits.cpu']);
|
||||
if (data.spec.hard && data.spec.hard[KubernetesPortainerResourceQuotaCPULimit]) {
|
||||
res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard[KubernetesPortainerResourceQuotaCPULimit]);
|
||||
}
|
||||
if (data.spec.hard && data.spec.hard['limits.memory']) {
|
||||
res.MemoryLimit = filesizeParser(data.spec.hard['limits.memory'], { base: 10 });
|
||||
if (data.spec.hard && data.spec.hard[KubernetesPortainerResourceQuotaMemoryLimit]) {
|
||||
res.MemoryLimit = filesizeParser(data.spec.hard[KubernetesPortainerResourceQuotaMemoryLimit], { base: 10 });
|
||||
}
|
||||
|
||||
res.MemoryLimitUsed = 0;
|
||||
if (data.status.used && data.status.used['limits.memory']) {
|
||||
res.MemoryLimitUsed = filesizeParser(data.status.used['limits.memory'], { base: 10 });
|
||||
if (data.status.used && data.status.used[KubernetesPortainerResourceQuotaMemoryLimit]) {
|
||||
res.MemoryLimitUsed = filesizeParser(data.status.used[KubernetesPortainerResourceQuotaMemoryLimit], { base: 10 });
|
||||
}
|
||||
|
||||
res.CpuLimitUsed = 0;
|
||||
if (data.status.used && data.status.used['limits.cpu']) {
|
||||
res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used['limits.cpu']);
|
||||
if (data.status.used && data.status.used[KubernetesPortainerResourceQuotaCPULimit]) {
|
||||
res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used[KubernetesPortainerResourceQuotaCPULimit]);
|
||||
}
|
||||
res.Yaml = yaml ? yaml.data : '';
|
||||
res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : '';
|
||||
@@ -40,48 +50,54 @@ class KubernetesResourceQuotaConverter {
|
||||
const res = new KubernetesResourceQuotaCreatePayload();
|
||||
res.metadata.name = KubernetesResourceQuotaHelper.generateResourceQuotaName(quota.Namespace);
|
||||
res.metadata.namespace = quota.Namespace;
|
||||
res.spec.hard['requests.cpu'] = quota.CpuLimit;
|
||||
res.spec.hard['requests.memory'] = quota.MemoryLimit;
|
||||
res.spec.hard['limits.cpu'] = quota.CpuLimit;
|
||||
res.spec.hard['limits.memory'] = quota.MemoryLimit;
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaCPURequest}']`, quota.CpuLimit);
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaMemoryRequest}']`, quota.MemoryLimit);
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaCPULimit}']`, quota.CpuLimit);
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaMemoryLimit}']`, quota.MemoryLimit);
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName;
|
||||
if (quota.ResourcePoolOwner) {
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner;
|
||||
}
|
||||
if (!quota.CpuLimit || quota.CpuLimit === 0) {
|
||||
delete res.spec.hard['requests.cpu'];
|
||||
delete res.spec.hard['limits.cpu'];
|
||||
}
|
||||
if (!quota.MemoryLimit || quota.MemoryLimit === 0) {
|
||||
delete res.spec.hard['requests.memory'];
|
||||
delete res.spec.hard['limits.memory'];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static updatePayload(quota) {
|
||||
const res = new KubernetesResourceQuotaUpdatePayload();
|
||||
res.metadata.name = quota.Name;
|
||||
res.metadata.namespace = quota.Namespace;
|
||||
const res = KubernetesResourceQuotaConverter.createPayload(quota);
|
||||
res.metadata.uid = quota.Id;
|
||||
res.spec.hard['requests.cpu'] = quota.CpuLimit;
|
||||
res.spec.hard['requests.memory'] = quota.MemoryLimit;
|
||||
res.spec.hard['limits.cpu'] = quota.CpuLimit;
|
||||
res.spec.hard['limits.memory'] = quota.MemoryLimit;
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName;
|
||||
if (quota.ResourcePoolOwner) {
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner;
|
||||
}
|
||||
if (!quota.CpuLimit || quota.CpuLimit === 0) {
|
||||
delete res.spec.hard['requests.cpu'];
|
||||
delete res.spec.hard['limits.cpu'];
|
||||
}
|
||||
if (!quota.MemoryLimit || quota.MemoryLimit === 0) {
|
||||
delete res.spec.hard['requests.memory'];
|
||||
delete res.spec.hard['limits.memory'];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static patchPayload(oldQuota, newQuota) {
|
||||
const oldPayload = KubernetesResourceQuotaConverter.createPayload(oldQuota);
|
||||
const newPayload = KubernetesResourceQuotaConverter.createPayload(newQuota);
|
||||
const payload = JsonPatch.compare(oldPayload, newPayload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
static quotaToResourcePoolFormValues(quota) {
|
||||
const res = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
||||
res.Name = quota.Namespace;
|
||||
res.CpuLimit = quota.CpuLimit;
|
||||
res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
|
||||
if (res.CpuLimit || res.MemoryLimit) {
|
||||
res.HasQuota = true;
|
||||
}
|
||||
res.StorageClasses = quota.StorageRequests;
|
||||
return res;
|
||||
}
|
||||
|
||||
static resourcePoolFormValuesToResourceQuota(formValues) {
|
||||
if (formValues.HasQuota) {
|
||||
const quota = new KubernetesResourceQuota(formValues.Name);
|
||||
if (formValues.HasQuota) {
|
||||
quota.CpuLimit = formValues.CpuLimit;
|
||||
quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
|
||||
}
|
||||
quota.ResourcePoolName = formValues.Name;
|
||||
quota.ResourcePoolOwner = formValues.Owner;
|
||||
return quota;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourceQuotaConverter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-class/payload';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
class KubernetesStorageClassConverter {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
@@ -322,7 +322,7 @@ class KubernetesApplicationHelper {
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage.slice(0, -2));
|
||||
res.Size = parseInt(pvc.Storage, 10);
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
return res;
|
||||
|
||||
@@ -16,5 +16,13 @@ class KubernetesCommonHelper {
|
||||
label = _.replace(label, /[-_.]*$/g, '');
|
||||
return label;
|
||||
}
|
||||
|
||||
static assignOrDeleteIfEmptyOrZero(obj, path, value) {
|
||||
if (!value || value === 0 || (value instanceof Array && !value.length)) {
|
||||
_.unset(obj, path);
|
||||
} else {
|
||||
_.set(obj, path, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default KubernetesCommonHelper;
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
class KubernetesFormValidationHelper {
|
||||
static getInvalidKeys(names) {
|
||||
const res = {};
|
||||
_.forEach(names, (name, index) => {
|
||||
const valid = /^[-._a-zA-Z0-9]+$/.test(name);
|
||||
if (!valid) {
|
||||
res[index] = true;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
static getDuplicates(names) {
|
||||
const groupped = _.groupBy(names);
|
||||
const res = {};
|
||||
|
||||
@@ -4,6 +4,28 @@ class KubernetesResourceQuotaHelper {
|
||||
static generateResourceQuotaName(name) {
|
||||
return KubernetesPortainerResourceQuotaPrefix + name;
|
||||
}
|
||||
|
||||
static formatBytes(bytes, decimals = 0, base10 = true) {
|
||||
const res = {
|
||||
Size: 0,
|
||||
SizeUnit: 'B',
|
||||
};
|
||||
|
||||
if (bytes === 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const k = base10 ? 1000 : 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return {
|
||||
Size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
|
||||
SizeUnit: sizes[i],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourceQuotaHelper;
|
||||
|
||||
@@ -26,7 +26,7 @@ class KubernetesResourceReservationHelper {
|
||||
}
|
||||
|
||||
static parseCPU(cpu) {
|
||||
let res = parseInt(cpu);
|
||||
let res = parseInt(cpu, 10);
|
||||
if (_.endsWith(cpu, 'm')) {
|
||||
res /= 1000;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
@@ -80,18 +80,18 @@ export class KubernetesIngressConverter {
|
||||
}
|
||||
res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name;
|
||||
res.Host = formValues.Host;
|
||||
res.Paths = formValues.Paths;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {KubernetesIngressClass} ics Ingress classes (saved in Portainer DB)
|
||||
* @param {KubernetesIngress} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and passed for RP EDIT VIEW
|
||||
* @param {KubernetesIngress[]} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and filled for RP EDIT VIEW
|
||||
*/
|
||||
static ingressClassesToFormValues(ics, ingresses) {
|
||||
const res = _.map(ics, (ic) => {
|
||||
const fv = new KubernetesResourcePoolIngressClassFormValue();
|
||||
fv.IngressClass = ic;
|
||||
const fv = new KubernetesResourcePoolIngressClassFormValue(ic);
|
||||
const ingress = _.find(ingresses, { Name: ic.Name });
|
||||
if (ingress) {
|
||||
fv.Selected = true;
|
||||
@@ -110,6 +110,7 @@ export class KubernetesIngressConverter {
|
||||
});
|
||||
fv.Annotations = _.without(annotations, undefined);
|
||||
fv.AdvancedConfig = fv.Annotations.length > 0;
|
||||
fv.Paths = ingress.Paths;
|
||||
}
|
||||
return fv;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
export class KubernetesIngressHelper {
|
||||
static findSBoundServiceIngressesRules(ingresses, serviceName) {
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||
import { KubernetesIngressConverter } from './converter';
|
||||
|
||||
class KubernetesIngressService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesIngresses) {
|
||||
this.$async = $async;
|
||||
this.KubernetesIngresses = KubernetesIngresses;
|
||||
/* @ngInject */
|
||||
export function KubernetesIngressService($async, KubernetesIngresses) {
|
||||
return {
|
||||
get,
|
||||
create,
|
||||
patch,
|
||||
delete: _delete,
|
||||
};
|
||||
|
||||
this.getAsync = this.getAsync.bind(this);
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async getAsync(namespace, name) {
|
||||
async function getOne(namespace, name) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
const [raw, yaml] = await Promise.all([this.KubernetesIngresses(namespace).get(params).$promise, this.KubernetesIngresses(namespace).getYaml(params).$promise]);
|
||||
const [raw, yaml] = await Promise.all([KubernetesIngresses(namespace).get(params).$promise, KubernetesIngresses(namespace).getYaml(params).$promise]);
|
||||
const res = {
|
||||
Raw: KubernetesIngressConverter.apiToModel(raw),
|
||||
Yaml: yaml.data,
|
||||
@@ -35,9 +28,9 @@ class KubernetesIngressService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
async function getAll(namespace) {
|
||||
try {
|
||||
const data = await this.KubernetesIngresses(namespace).get().$promise;
|
||||
const data = await KubernetesIngresses(namespace).get().$promise;
|
||||
const res = _.reduce(data.items, (arr, item) => _.concat(arr, KubernetesIngressConverter.apiToModel(item)), []);
|
||||
return res;
|
||||
} catch (err) {
|
||||
@@ -45,73 +38,57 @@ class KubernetesIngressService {
|
||||
}
|
||||
}
|
||||
|
||||
get(namespace, name) {
|
||||
function get(namespace, name) {
|
||||
if (name) {
|
||||
return this.$async(this.getAsync, namespace, name);
|
||||
return $async(getOne, namespace, name);
|
||||
}
|
||||
return this.$async(this.getAllAsync, namespace);
|
||||
return $async(getAll, namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE
|
||||
*/
|
||||
async createAsync(ingress) {
|
||||
try {
|
||||
const params = {};
|
||||
const payload = KubernetesIngressConverter.createPayload(ingress);
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create ingress', err);
|
||||
}
|
||||
}
|
||||
|
||||
create(ingress) {
|
||||
return this.$async(this.createAsync, ingress);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH
|
||||
*/
|
||||
async patchAsync(oldIngress, newIngress) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = newIngress.Name;
|
||||
const namespace = newIngress.Namespace;
|
||||
const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress);
|
||||
if (!payload.length) {
|
||||
return;
|
||||
function create(ingress) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const params = {};
|
||||
const payload = KubernetesIngressConverter.createPayload(ingress);
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await KubernetesIngresses(namespace).create(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create ingress', err);
|
||||
}
|
||||
const data = await this.KubernetesIngresses(namespace).patch(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to patch ingress', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patch(oldIngress, newIngress) {
|
||||
return this.$async(this.patchAsync, oldIngress, newIngress);
|
||||
function patch(oldIngress, newIngress) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = newIngress.Name;
|
||||
const namespace = newIngress.Namespace;
|
||||
const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress);
|
||||
if (!payload.length) {
|
||||
return;
|
||||
}
|
||||
const data = await KubernetesIngresses(namespace).patch(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to patch ingress', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(ingress) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = ingress.IngressClass.Name;
|
||||
const namespace = ingress.Namespace;
|
||||
await this.KubernetesIngresses(namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to delete ingress', err);
|
||||
}
|
||||
}
|
||||
|
||||
delete(ingress) {
|
||||
return this.$async(this.deleteAsync, ingress);
|
||||
function _delete(ingress) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = ingress.Name;
|
||||
const namespace = ingress.Namespace;
|
||||
await KubernetesIngresses(namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to delete ingress', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesIngressService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesIngressService', KubernetesIngressService);
|
||||
|
||||
@@ -153,9 +153,9 @@ export class KubernetesApplicationAutoScalerFormValue {
|
||||
}
|
||||
}
|
||||
|
||||
export function KubernetesFormValueDuplicate() {
|
||||
export function KubernetesFormValidationReferences() {
|
||||
return {
|
||||
refs: {},
|
||||
hasDuplicates: false,
|
||||
hasRefs: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
/**
|
||||
* KubernetesNamespace Model
|
||||
*/
|
||||
const _KubernetesNamespace = Object.freeze({
|
||||
Id: '',
|
||||
Name: '',
|
||||
CreationDate: '',
|
||||
Status: '',
|
||||
Yaml: '',
|
||||
ResourcePoolName: '',
|
||||
ResourcePoolOwner: '',
|
||||
});
|
||||
|
||||
export class KubernetesNamespace {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespace)));
|
||||
}
|
||||
export function KubernetesNamespace() {
|
||||
return {
|
||||
Id: '',
|
||||
Name: '',
|
||||
CreationDate: '',
|
||||
Status: '',
|
||||
Yaml: '',
|
||||
ResourcePoolName: '',
|
||||
ResourcePoolOwner: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export function KubernetesResourcePoolFormValues(defaults) {
|
||||
return {
|
||||
Name: '',
|
||||
MemoryLimit: defaults.MemoryLimit,
|
||||
CpuLimit: defaults.CpuLimit,
|
||||
HasQuota: true,
|
||||
HasQuota: false,
|
||||
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
|
||||
};
|
||||
}
|
||||
@@ -20,6 +21,7 @@ export function KubernetesResourcePoolIngressClassFormValue(ingressClass) {
|
||||
Selected: false,
|
||||
WasSelected: false,
|
||||
AdvancedConfig: false,
|
||||
Paths: [], // will be filled to save IngressClass.Paths inside ingressClassesToFormValues() on RP EDIT
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||
|
||||
export const KubernetesPortainerResourceQuotaPrefix = 'portainer-rq-';
|
||||
export const KubernetesPortainerResourceQuotaCPULimit = 'limits.cpu';
|
||||
export const KubernetesPortainerResourceQuotaMemoryLimit = 'limits.memory';
|
||||
export const KubernetesPortainerResourceQuotaCPURequest = 'requests.cpu';
|
||||
export const KubernetesPortainerResourceQuotaMemoryRequest = 'requests.memory';
|
||||
|
||||
export const KubernetesResourceQuotaDefaults = {
|
||||
CpuLimit: 0,
|
||||
MemoryLimit: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* KubernetesResourceQuota Model
|
||||
*/
|
||||
const _KubernetesResourceQuota = Object.freeze({
|
||||
Id: '',
|
||||
Namespace: '',
|
||||
Name: '',
|
||||
CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit,
|
||||
MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit,
|
||||
CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit,
|
||||
MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit,
|
||||
Yaml: '',
|
||||
ResourcePoolName: '',
|
||||
ResourcePoolOwner: '',
|
||||
});
|
||||
|
||||
export class KubernetesResourceQuota {
|
||||
constructor(namespace) {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuota)));
|
||||
if (namespace) {
|
||||
this.Name = KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace);
|
||||
this.Namespace = namespace;
|
||||
}
|
||||
}
|
||||
export function KubernetesResourceQuota(namespace) {
|
||||
return {
|
||||
Id: '',
|
||||
Namespace: namespace ? namespace : '',
|
||||
Name: namespace ? KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace) : '',
|
||||
CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit,
|
||||
MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit,
|
||||
CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit,
|
||||
MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit,
|
||||
Yaml: '',
|
||||
ResourcePoolName: '',
|
||||
ResourcePoolOwner: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
|
||||
import {
|
||||
KubernetesPortainerResourceQuotaCPURequest,
|
||||
KubernetesPortainerResourceQuotaMemoryRequest,
|
||||
KubernetesPortainerResourceQuotaCPULimit,
|
||||
KubernetesPortainerResourceQuotaMemoryLimit,
|
||||
} from './models';
|
||||
|
||||
/**
|
||||
* KubernetesResourceQuotaCreatePayload Model
|
||||
*/
|
||||
const _KubernetesResourceQuotaCreatePayload = Object.freeze({
|
||||
metadata: new KubernetesCommonMetadataPayload(),
|
||||
spec: {
|
||||
hard: {
|
||||
'requests.cpu': 0,
|
||||
'requests.memory': 0,
|
||||
'limits.cpu': 0,
|
||||
'limits.memory': 0,
|
||||
export function KubernetesResourceQuotaCreatePayload() {
|
||||
return {
|
||||
metadata: new KubernetesCommonMetadataPayload(),
|
||||
spec: {
|
||||
hard: {
|
||||
[KubernetesPortainerResourceQuotaCPURequest]: 0,
|
||||
[KubernetesPortainerResourceQuotaMemoryRequest]: 0,
|
||||
[KubernetesPortainerResourceQuotaCPULimit]: 0,
|
||||
[KubernetesPortainerResourceQuotaMemoryLimit]: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export class KubernetesResourceQuotaCreatePayload {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaCreatePayload)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KubernetesResourceQuotaUpdatePayload Model
|
||||
*/
|
||||
const _KubernetesResourceQuotaUpdatePayload = Object.freeze({
|
||||
metadata: new KubernetesCommonMetadataPayload(),
|
||||
spec: {
|
||||
hard: {
|
||||
'requests.cpu': 0,
|
||||
'requests.memory': 0,
|
||||
'limits.cpu': 0,
|
||||
'limits.memory': 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export class KubernetesResourceQuotaUpdatePayload {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaUpdatePayload)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [
|
||||
},
|
||||
create: { method: 'POST' },
|
||||
update: { method: 'PUT' },
|
||||
patch: {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
},
|
||||
},
|
||||
delete: { method: 'DELETE' },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import angular from 'angular';
|
||||
import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool';
|
||||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
|
||||
class KubernetesResourcePoolService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
|
||||
this.$async = $async;
|
||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
/* @ngInject */
|
||||
export function KubernetesResourcePoolService($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
|
||||
return {
|
||||
get,
|
||||
create,
|
||||
patch,
|
||||
delete: _delete,
|
||||
};
|
||||
|
||||
this.getAsync = this.getAsync.bind(this);
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async getAsync(name) {
|
||||
async function getOne(name) {
|
||||
try {
|
||||
const namespace = await this.KubernetesNamespaceService.get(name);
|
||||
const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const namespace = await KubernetesNamespaceService.get(name);
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
@@ -41,13 +28,13 @@ class KubernetesResourcePoolService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllAsync() {
|
||||
async function getAll() {
|
||||
try {
|
||||
const namespaces = await this.KubernetesNamespaceService.get();
|
||||
const namespaces = await KubernetesNamespaceService.get();
|
||||
const pools = await Promise.all(
|
||||
_.map(namespaces, async (namespace) => {
|
||||
const name = namespace.Name;
|
||||
const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
@@ -62,66 +49,75 @@ class KubernetesResourcePoolService {
|
||||
}
|
||||
}
|
||||
|
||||
get(name) {
|
||||
function get(name) {
|
||||
if (name) {
|
||||
return this.$async(this.getAsync, name);
|
||||
return $async(getOne, name);
|
||||
}
|
||||
return this.$async(this.getAllAsync);
|
||||
return $async(getAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE
|
||||
* @param {KubernetesResourcePoolFormValues} formValues
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
formValues.Owner = KubernetesCommonHelper.ownerToLabel(formValues.Owner);
|
||||
function create(formValues) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const [namespace, quota, ingresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
|
||||
await KubernetesNamespaceService.create(namespace);
|
||||
|
||||
try {
|
||||
const namespace = new KubernetesNamespace();
|
||||
namespace.Name = formValues.Name;
|
||||
namespace.ResourcePoolName = formValues.Name;
|
||||
namespace.ResourcePoolOwner = formValues.Owner;
|
||||
await this.KubernetesNamespaceService.create(namespace);
|
||||
if (formValues.HasQuota) {
|
||||
const quota = new KubernetesResourceQuota(formValues.Name);
|
||||
quota.CpuLimit = formValues.CpuLimit;
|
||||
quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
|
||||
quota.ResourcePoolName = formValues.Name;
|
||||
quota.ResourcePoolOwner = formValues.Owner;
|
||||
await this.KubernetesResourceQuotaService.create(quota);
|
||||
}
|
||||
const ingressPromises = _.map(formValues.IngressClasses, (c) => {
|
||||
if (c.Selected) {
|
||||
c.Namespace = namespace.Name;
|
||||
const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
return this.KubernetesIngressService.create(ingress);
|
||||
if (quota) {
|
||||
await KubernetesResourceQuotaService.create(quota);
|
||||
}
|
||||
});
|
||||
await Promise.all(ingressPromises);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i));
|
||||
await Promise.all(ingressPromises);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(formValues) {
|
||||
return this.$async(this.createAsync, formValues);
|
||||
function patch(oldFormValues, newFormValues) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
|
||||
const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
|
||||
void oldNamespace, newNamespace;
|
||||
|
||||
if (oldQuota && newQuota) {
|
||||
await KubernetesResourceQuotaService.patch(oldQuota, newQuota);
|
||||
} else if (!oldQuota && newQuota) {
|
||||
await KubernetesResourceQuotaService.create(newQuota);
|
||||
} else if (oldQuota && !newQuota) {
|
||||
await KubernetesResourceQuotaService.delete(oldQuota);
|
||||
}
|
||||
|
||||
const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name }));
|
||||
const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name }));
|
||||
const patch = _.without(newIngresses, ...create);
|
||||
|
||||
const createPromises = _.map(create, (i) => KubernetesIngressService.create(i));
|
||||
const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i));
|
||||
const patchPromises = _.map(patch, (ing) => {
|
||||
const old = _.find(oldIngresses, { Name: ing.Name });
|
||||
ing.Paths = angular.copy(old.Paths);
|
||||
ing.PreviousHost = old.Host;
|
||||
return KubernetesIngressService.patch(old, ing);
|
||||
});
|
||||
|
||||
const promises = _.flatten([createPromises, delPromises, patchPromises]);
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(pool) {
|
||||
try {
|
||||
await this.KubernetesNamespaceService.delete(pool.Namespace);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
delete(pool) {
|
||||
return this.$async(this.deleteAsync, pool);
|
||||
function _delete(pool) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
await KubernetesNamespaceService.delete(pool.Namespace);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService);
|
||||
|
||||
@@ -5,105 +5,85 @@ import PortainerError from 'Portainer/error';
|
||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||
|
||||
class KubernetesResourceQuotaService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesResourceQuotas) {
|
||||
this.$async = $async;
|
||||
this.KubernetesResourceQuotas = KubernetesResourceQuotas;
|
||||
/* @ngInject */
|
||||
export function KubernetesResourceQuotaService($async, KubernetesResourceQuotas) {
|
||||
return {
|
||||
get,
|
||||
create,
|
||||
patch,
|
||||
delete: _delete,
|
||||
};
|
||||
|
||||
this.getAsync = this.getAsync.bind(this);
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.updateAsync = this.updateAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async getAsync(namespace, name) {
|
||||
async function getOne(namespace, name) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
const [raw, yaml] = await Promise.all([this.KubernetesResourceQuotas(namespace).get(params).$promise, this.KubernetesResourceQuotas(namespace).getYaml(params).$promise]);
|
||||
const [raw, yaml] = await Promise.all([KubernetesResourceQuotas(namespace).get(params).$promise, KubernetesResourceQuotas(namespace).getYaml(params).$promise]);
|
||||
return KubernetesResourceQuotaConverter.apiToResourceQuota(raw, yaml);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve resource quota', err);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
async function getAll(namespace) {
|
||||
try {
|
||||
const data = await this.KubernetesResourceQuotas(namespace).get().$promise;
|
||||
const data = await KubernetesResourceQuotas(namespace).get().$promise;
|
||||
return _.map(data.items, (item) => KubernetesResourceQuotaConverter.apiToResourceQuota(item));
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve resource quotas', err);
|
||||
}
|
||||
}
|
||||
|
||||
get(namespace, name) {
|
||||
function get(namespace, name) {
|
||||
if (name) {
|
||||
return this.$async(this.getAsync, namespace, name);
|
||||
return $async(getOne, namespace, name);
|
||||
}
|
||||
return this.$async(this.getAllAsync, namespace);
|
||||
return $async(getAll, namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE
|
||||
*/
|
||||
async createAsync(quota) {
|
||||
try {
|
||||
const payload = KubernetesResourceQuotaConverter.createPayload(quota);
|
||||
const namespace = payload.metadata.namespace;
|
||||
const params = {};
|
||||
const data = await this.KubernetesResourceQuotas(namespace).create(params, payload).$promise;
|
||||
return KubernetesResourceQuotaConverter.apiToResourceQuota(data);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create quota', err);
|
||||
}
|
||||
function create(quota) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const payload = KubernetesResourceQuotaConverter.createPayload(quota);
|
||||
const namespace = payload.metadata.namespace;
|
||||
const params = {};
|
||||
const data = await KubernetesResourceQuotas(namespace).create(params, payload).$promise;
|
||||
return KubernetesResourceQuotaConverter.apiToResourceQuota(data);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create quota', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(quota) {
|
||||
return this.$async(this.createAsync, quota);
|
||||
function patch(oldQuota, newQuota) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = newQuota.Name;
|
||||
const namespace = newQuota.Namespace;
|
||||
const payload = KubernetesResourceQuotaConverter.patchPayload(oldQuota, newQuota);
|
||||
if (!payload.length) {
|
||||
return;
|
||||
}
|
||||
const data = await KubernetesResourceQuotas(namespace).patch(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to update resource quota', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE
|
||||
*/
|
||||
async updateAsync(quota) {
|
||||
try {
|
||||
const payload = KubernetesResourceQuotaConverter.updatePayload(quota);
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = payload.metadata.name;
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await this.KubernetesResourceQuotas(namespace).update(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to update resource quota', err);
|
||||
}
|
||||
}
|
||||
|
||||
update(quota) {
|
||||
return this.$async(this.updateAsync, quota);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(quota) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = quota.Name;
|
||||
await this.KubernetesResourceQuotas(quota.Namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to delete quota', err);
|
||||
}
|
||||
}
|
||||
|
||||
delete(quota) {
|
||||
return this.$async(this.deleteAsync, quota);
|
||||
function _delete(quota) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = quota.Name;
|
||||
await KubernetesResourceQuotas(quota.Namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to delete quota', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourceQuotaService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesResourceQuotaService', KubernetesResourceQuotaService);
|
||||
|
||||
@@ -21,7 +21,7 @@ class KubernetesVolumeService {
|
||||
*/
|
||||
async getAsync(namespace, name) {
|
||||
try {
|
||||
const [pvc, pool] = await Promise.all([await this.KubernetesPersistentVolumeClaimService.get(namespace, name), await this.KubernetesResourcePoolService.get(namespace)]);
|
||||
const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]);
|
||||
return KubernetesVolumeConverter.pvcToVolume(pvc, pool);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
@@ -30,7 +30,8 @@ class KubernetesVolumeService {
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const pools = await this.KubernetesResourcePoolService.get(namespace);
|
||||
const data = await this.KubernetesResourcePoolService.get(namespace);
|
||||
const pools = data instanceof Array ? data : [data];
|
||||
const res = await Promise.all(
|
||||
_.map(pools, async (pool) => {
|
||||
const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require('../../templates/advancedDeploymentPanel.html');
|
||||
|
||||
import angular from 'angular';
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
|
||||
|
||||
@@ -143,60 +143,72 @@
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables | orderBy: 'NameIndex'" style="margin-top: 2px;">
|
||||
<div class="col-sm-4 input-group input-group-sm" style="vertical-align: top;">
|
||||
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">name</span>
|
||||
<div style="margin-top: 2px;">
|
||||
<div class="col-sm-4 input-group input-group-sm">
|
||||
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input
|
||||
type="text"
|
||||
name="environment_variable_name_{{ $index }}"
|
||||
class="form-control"
|
||||
ng-model="envVar.Name"
|
||||
ng-change="ctrl.onChangeEnvironmentName()"
|
||||
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
|
||||
placeholder="foo"
|
||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 input-group input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input
|
||||
type="text"
|
||||
name="environment_variable_name_{{ $index }}"
|
||||
name="environment_variable_value_{{ $index }}"
|
||||
class="form-control"
|
||||
ng-model="envVar.Name"
|
||||
ng-change="ctrl.onChangeEnvironmentName()"
|
||||
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
|
||||
placeholder="foo"
|
||||
ng-model="envVar.Value"
|
||||
placeholder="bar"
|
||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist alphanumeric characters, '-' or '_', start with an alphabetic
|
||||
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
|
||||
>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
|
||||
>
|
||||
|
||||
<div class="col-sm-2 input-group input-group-sm" ng-if="ctrl.formValues.Containers.length <= 1">
|
||||
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable(envVar)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable(envVar)">
|
||||
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-4 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input
|
||||
type="text"
|
||||
name="environment_variable_value_{{ $index }}"
|
||||
class="form-control"
|
||||
ng-model="envVar.Value"
|
||||
placeholder="bar"
|
||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-2 input-group-sm" ng-if="ctrl.formValues.Containers.length <= 1">
|
||||
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable(envVar)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable(envVar)">
|
||||
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
|
||||
"
|
||||
>
|
||||
<div class="col-sm-4 input-group input-group-sm">
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist alphanumeric characters, '-' or '_', start with an alphabetic
|
||||
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
|
||||
>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4 input-group input-group-sm"></div>
|
||||
<div class="col-sm-2 input-group input-group-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,54 +284,65 @@
|
||||
<!-- has-override -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="config.Overriden">
|
||||
<div ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px;">
|
||||
<div class="col-md-1 col-sm-2" style="margin-left: 3px;" style="vertical-align: top;"></div>
|
||||
<div class="input-group col-sm-3 input-group-sm" style="vertical-align: top;">
|
||||
<span class="input-group-addon">configuration key</span>
|
||||
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
|
||||
<div style="margin-top: 2px;">
|
||||
<div class="col-sm-1 input-group input-group-sm" style="margin-left: 3px;"></div>
|
||||
<div class="col-sm-3 input-group input-group-sm">
|
||||
<span class="input-group-addon">configuration key</span>
|
||||
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 input-group input-group-sm" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||
<div class="col-sm-12 input-group input-group-sm">
|
||||
<span class="input-group-addon">path on disk</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="overridenKey.Path"
|
||||
placeholder="/etc/myapp/conf.d"
|
||||
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
|
||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||
required
|
||||
ng-change="ctrl.onChangeConfigurationPath()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-4 btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
|
||||
<i class="fa fa-list" aria-hidden="true"></i> Environment
|
||||
</label>
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||
<i class="fa fa-file" aria-hidden="true"></i> Filesystem
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
style="vertical-align: top;"
|
||||
ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||
"
|
||||
>
|
||||
<div class="input-group col-sm-12 input-group-sm">
|
||||
<span class="input-group-addon">path on disk</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="overridenKey.Path"
|
||||
placeholder="/etc/myapp/conf.d"
|
||||
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
|
||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||
required
|
||||
ng-change="ctrl.onChangeConfigurationPath()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
|
||||
<div class="col-sm-1 input-group input-group-sm" style="margin-left: 3px;"></div>
|
||||
<div class="col-sm-3 input-group input-group-sm"></div>
|
||||
<div class="col-sm-3 input-group input-group-sm" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 btn-group btn-group-sm" style="vertical-align: top;">
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
|
||||
<i class="fa fa-list" aria-hidden="true"></i> Environment
|
||||
</label>
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||
<i class="fa fa-file" aria-hidden="true"></i> Filesystem
|
||||
</label>
|
||||
<div class="col-sm-4 input-group input-group-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,12 +365,7 @@
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Persisted folders</label>
|
||||
<span
|
||||
class="label label-default interactive"
|
||||
style="margin-left: 10px;"
|
||||
ng-click="ctrl.addPersistedFolder()"
|
||||
ng-if="!ctrl.isEditAndStatefulSet() && ctrl.formValues.Containers.length <= 1"
|
||||
>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPersistedFolder()" ng-if="ctrl.isAddPersistentFolderButtonShowed()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add persisted folder
|
||||
</span>
|
||||
</div>
|
||||
@@ -383,7 +401,7 @@
|
||||
ng-model="persistedFolder.UseNewVolume"
|
||||
uib-btn-radio="true"
|
||||
ng-change="ctrl.useNewVolume($index)"
|
||||
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||
ng-disabled="ctrl.isNewVolumeButtonDisabled($index)"
|
||||
>New volume</label
|
||||
>
|
||||
<label
|
||||
@@ -391,7 +409,7 @@
|
||||
ng-model="persistedFolder.UseNewVolume"
|
||||
uib-btn-radio="false"
|
||||
ng-change="ctrl.useExistingVolume($index)"
|
||||
ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET"
|
||||
ng-disabled="ctrl.isExistingVolumeButtonDisabled()"
|
||||
>Existing volume</label
|
||||
>
|
||||
</span>
|
||||
@@ -419,12 +437,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="input-group col-sm-2 input-group-sm"
|
||||
ng-class="{ striked: persistedFolder.NeedsDeletion }"
|
||||
style="vertical-align: top;"
|
||||
ng-if="persistedFolder.UseNewVolume"
|
||||
>
|
||||
<div class="input-group col-sm-2 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
|
||||
<span class="input-group-addon">storage</span>
|
||||
<select
|
||||
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
|
||||
@@ -452,7 +465,7 @@
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
|
||||
<div ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
|
||||
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -489,7 +502,7 @@
|
||||
|
||||
<div class="input-group col-sm-2 input-group-sm"></div>
|
||||
|
||||
<div class="input-group col-sm-2 input-group-sm">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-show="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid">
|
||||
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Size is required.</p>
|
||||
@@ -509,8 +522,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm"> </div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm"> </div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -569,7 +580,7 @@
|
||||
</div>
|
||||
<div
|
||||
ng-if="
|
||||
(!ctrl.state.isEdit && !ctrl.state.PersistedFoldersUseExistingVolumes) ||
|
||||
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
|
||||
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)
|
||||
"
|
||||
>
|
||||
@@ -590,7 +601,7 @@
|
||||
</div>
|
||||
<div
|
||||
style="color: #767676;"
|
||||
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.PersistedFoldersUseExistingVolumes"
|
||||
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes"
|
||||
>
|
||||
<input type="radio" id="data_access_isolated" disabled />
|
||||
<label
|
||||
@@ -679,7 +690,8 @@
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="kubernetesApplicationCreationForm.memory_limit.$error">
|
||||
<p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.state.sliders.memory.min }} and {{ ctrl.state.sliders.memory.max }}
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.state.sliders.memory.min }} and
|
||||
{{ ctrl.state.sliders.memory.max }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,7 +798,7 @@
|
||||
style="margin-left: 20px;"
|
||||
ng-model="ctrl.formValues.ReplicaCount"
|
||||
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
|
||||
ng-change="ctrl.onChangeVolumeRequestedSize()"
|
||||
ng-change="ctrl.enforceReplicaCountMinimum()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -807,7 +819,8 @@
|
||||
>
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application will reserve the following resources: <b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
|
||||
This application will reserve the following resources:
|
||||
<b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
|
||||
<b>{{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB</b> of memory.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import angular from 'angular';
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
KubernetesApplicationPersistedFolderFormValue,
|
||||
KubernetesApplicationPublishedPortFormValue,
|
||||
KubernetesApplicationPlacementFormValue,
|
||||
KubernetesFormValueDuplicate,
|
||||
KubernetesFormValidationReferences,
|
||||
} from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||
@@ -75,15 +75,8 @@ class KubernetesCreateApplicationController {
|
||||
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
||||
this.ServiceTypes = KubernetesServiceTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
|
||||
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
|
||||
this.updateSlidersAsync = this.updateSlidersAsync.bind(this);
|
||||
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
|
||||
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
|
||||
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
|
||||
this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this);
|
||||
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -92,7 +85,7 @@ class KubernetesCreateApplicationController {
|
||||
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
||||
}
|
||||
|
||||
/* #region AUTO SCLAER UI MANAGEMENT */
|
||||
/* #region AUTO SCALER UI MANAGEMENT */
|
||||
unselectAutoScaler() {
|
||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
|
||||
this.formValues.AutoScaler.IsUsed = false;
|
||||
@@ -156,7 +149,7 @@ class KubernetesCreateApplicationController {
|
||||
});
|
||||
});
|
||||
|
||||
this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
|
||||
this.state.duplicates.configurationPaths.hasRefs = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -184,7 +177,7 @@ class KubernetesCreateApplicationController {
|
||||
|
||||
onChangeEnvironmentName() {
|
||||
this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
|
||||
this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
|
||||
this.state.duplicates.environmentVariables.hasRefs = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -195,12 +188,14 @@ class KubernetesCreateApplicationController {
|
||||
storageClass = this.storageClasses[0];
|
||||
}
|
||||
|
||||
this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass));
|
||||
const newPf = new KubernetesApplicationPersistedFolderFormValue(storageClass);
|
||||
this.formValues.PersistedFolders.push(newPf);
|
||||
this.resetDeploymentType();
|
||||
}
|
||||
|
||||
restorePersistedFolder(index) {
|
||||
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
||||
this.validatePersistedFolders();
|
||||
}
|
||||
|
||||
resetPersistedFolders() {
|
||||
@@ -208,6 +203,7 @@ class KubernetesCreateApplicationController {
|
||||
persistedFolder.ExistingVolume = null;
|
||||
persistedFolder.UseNewVolume = true;
|
||||
});
|
||||
this.validatePersistedFolders();
|
||||
}
|
||||
|
||||
removePersistedFolder(index) {
|
||||
@@ -216,6 +212,29 @@ class KubernetesCreateApplicationController {
|
||||
} else {
|
||||
this.formValues.PersistedFolders.splice(index, 1);
|
||||
}
|
||||
this.validatePersistedFolders();
|
||||
}
|
||||
|
||||
useNewVolume(index) {
|
||||
this.formValues.PersistedFolders[index].UseNewVolume = true;
|
||||
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
||||
this.state.persistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
|
||||
this.validatePersistedFolders();
|
||||
}
|
||||
|
||||
useExistingVolume(index) {
|
||||
this.formValues.PersistedFolders[index].UseNewVolume = false;
|
||||
this.state.persistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
|
||||
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
||||
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
||||
this.resetDeploymentType();
|
||||
}
|
||||
this.validatePersistedFolders();
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region PERSISTENT FOLDERS ON CHANGE VALIDATION */
|
||||
validatePersistedFolders() {
|
||||
this.onChangePersistedFolderPath();
|
||||
this.onChangeExistingVolumeSelection();
|
||||
}
|
||||
@@ -229,31 +248,19 @@ class KubernetesCreateApplicationController {
|
||||
return persistedFolder.ContainerPath;
|
||||
})
|
||||
);
|
||||
this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
||||
this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
||||
}
|
||||
|
||||
onChangeExistingVolumeSelection() {
|
||||
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
|
||||
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||
if (persistedFolder.NeedsDeletion) {
|
||||
return undefined;
|
||||
}
|
||||
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
|
||||
})
|
||||
);
|
||||
this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
|
||||
}
|
||||
|
||||
useNewVolume(index) {
|
||||
this.formValues.PersistedFolders[index].UseNewVolume = true;
|
||||
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
||||
this.state.PersistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
|
||||
}
|
||||
|
||||
useExistingVolume(index) {
|
||||
this.formValues.PersistedFolders[index].UseNewVolume = false;
|
||||
this.state.PersistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
|
||||
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
||||
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
||||
this.resetDeploymentType();
|
||||
}
|
||||
this.state.duplicates.existingVolumes.hasRefs = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -296,7 +303,7 @@ class KubernetesCreateApplicationController {
|
||||
const source = _.map(this.formValues.Placements, (p) => (p.NeedsDeletion ? undefined : p.Label.Key));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -351,10 +358,10 @@ class KubernetesCreateApplicationController {
|
||||
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
} else {
|
||||
state.refs = {};
|
||||
state.hasDuplicates = false;
|
||||
state.hasRefs = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,10 +371,10 @@ class KubernetesCreateApplicationController {
|
||||
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
} else {
|
||||
state.refs = {};
|
||||
state.hasDuplicates = false;
|
||||
state.hasRefs = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,9 +389,9 @@ class KubernetesCreateApplicationController {
|
||||
const state = this.state.duplicates.publishedPorts.ingressRoutes;
|
||||
|
||||
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
||||
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
||||
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path));
|
||||
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined));
|
||||
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined));
|
||||
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => `${p.Host || i.Name}${p.Path}`));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
|
||||
_.forEach(newRoutes, (route, idx) => {
|
||||
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
|
||||
@@ -392,10 +399,10 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
} else {
|
||||
state.refs = {};
|
||||
state.hasDuplicates = false;
|
||||
state.hasRefs = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,10 +412,10 @@ class KubernetesCreateApplicationController {
|
||||
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
} else {
|
||||
state.refs = {};
|
||||
state.hasDuplicates = false;
|
||||
state.hasRefs = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,14 +435,14 @@ class KubernetesCreateApplicationController {
|
||||
isValid() {
|
||||
return (
|
||||
!this.state.alreadyExists &&
|
||||
!this.state.duplicates.environmentVariables.hasDuplicates &&
|
||||
!this.state.duplicates.persistedFolders.hasDuplicates &&
|
||||
!this.state.duplicates.configurationPaths.hasDuplicates &&
|
||||
!this.state.duplicates.existingVolumes.hasDuplicates &&
|
||||
!this.state.duplicates.publishedPorts.containerPorts.hasDuplicates &&
|
||||
!this.state.duplicates.publishedPorts.nodePorts.hasDuplicates &&
|
||||
!this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates &&
|
||||
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates
|
||||
!this.state.duplicates.environmentVariables.hasRefs &&
|
||||
!this.state.duplicates.persistedFolders.hasRefs &&
|
||||
!this.state.duplicates.configurationPaths.hasRefs &&
|
||||
!this.state.duplicates.existingVolumes.hasRefs &&
|
||||
!this.state.duplicates.publishedPorts.containerPorts.hasRefs &&
|
||||
!this.state.duplicates.publishedPorts.nodePorts.hasRefs &&
|
||||
!this.state.duplicates.publishedPorts.ingressRoutes.hasRefs &&
|
||||
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasRefs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -501,12 +508,20 @@ class KubernetesCreateApplicationController {
|
||||
|
||||
if (folder.StorageClass && _.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
|
||||
storageOptions.push(folder.StorageClass.Name);
|
||||
} else {
|
||||
storageOptions.push('<no storage option available>');
|
||||
}
|
||||
}
|
||||
|
||||
return _.uniq(storageOptions).join(', ');
|
||||
}
|
||||
|
||||
enforceReplicaCountMinimum() {
|
||||
if (this.formValues.ReplicaCount === null) {
|
||||
this.formValues.ReplicaCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
resourceQuotaCapacityExceeded() {
|
||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||
}
|
||||
@@ -562,9 +577,29 @@ class KubernetesCreateApplicationController {
|
||||
return !this.editChanges.length;
|
||||
}
|
||||
|
||||
/* #region PERSISTED FOLDERS */
|
||||
/* #region BUTTONS STATES */
|
||||
isAddPersistentFolderButtonShowed() {
|
||||
return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1;
|
||||
}
|
||||
|
||||
isNewVolumeButtonDisabled(index) {
|
||||
return this.isEditAndExistingPersistedFolder(index);
|
||||
}
|
||||
|
||||
isExistingVolumeButtonDisabled() {
|
||||
return !this.hasAvailableVolumes() || (this.isEdit && this.application.ApplicationType === this.ApplicationTypes.STATEFULSET);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
hasAvailableVolumes() {
|
||||
return this.availableVolumes.length > 0;
|
||||
}
|
||||
|
||||
isEditAndExistingPersistedFolder(index) {
|
||||
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
isEditAndNotNewPublishedPort(index) {
|
||||
return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew;
|
||||
@@ -639,126 +674,126 @@ class KubernetesCreateApplicationController {
|
||||
/* #endregion */
|
||||
|
||||
/* #region DATA AUTO REFRESH */
|
||||
async updateSlidersAsync() {
|
||||
try {
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
let minCpu,
|
||||
maxCpu,
|
||||
minMemory,
|
||||
maxMemory = 0;
|
||||
if (quota) {
|
||||
updateSliders() {
|
||||
this.state.resourcePoolHasQuota = false;
|
||||
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
let minCpu,
|
||||
maxCpu,
|
||||
minMemory,
|
||||
maxMemory = 0;
|
||||
if (quota) {
|
||||
if (quota.CpuLimit) {
|
||||
this.state.resourcePoolHasQuota = true;
|
||||
if (quota.CpuLimit) {
|
||||
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
||||
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
|
||||
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
}
|
||||
if (quota.MemoryLimit) {
|
||||
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
|
||||
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
|
||||
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
||||
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
|
||||
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
this.state.resourcePoolHasQuota = false;
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
}
|
||||
if (quota.MemoryLimit) {
|
||||
this.state.resourcePoolHasQuota = true;
|
||||
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
|
||||
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
|
||||
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
this.state.sliders.memory.min = minMemory;
|
||||
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
||||
this.state.sliders.cpu.min = minCpu;
|
||||
this.state.sliders.cpu.max = _.round(maxCpu, 2);
|
||||
if (!this.state.isEdit) {
|
||||
this.formValues.CpuLimit = minCpu;
|
||||
this.formValues.MemoryLimit = minMemory;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update resources selector');
|
||||
} else {
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
}
|
||||
|
||||
updateSliders() {
|
||||
return this.$async(this.updateSlidersAsync);
|
||||
}
|
||||
|
||||
async refreshStacksAsync(namespace) {
|
||||
try {
|
||||
this.stacks = await this.KubernetesStackService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
||||
this.state.sliders.memory.min = minMemory;
|
||||
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
||||
this.state.sliders.cpu.min = minCpu;
|
||||
this.state.sliders.cpu.max = _.round(maxCpu, 2);
|
||||
if (!this.state.isEdit) {
|
||||
this.formValues.CpuLimit = minCpu;
|
||||
this.formValues.MemoryLimit = minMemory;
|
||||
}
|
||||
}
|
||||
|
||||
refreshStacks(namespace) {
|
||||
return this.$async(this.refreshStacksAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshConfigurationsAsync(namespace) {
|
||||
try {
|
||||
this.configurations = await this.KubernetesConfigurationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
}
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.stacks = await this.KubernetesStackService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshConfigurations(namespace) {
|
||||
return this.$async(this.refreshConfigurationsAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshApplicationsAsync(namespace) {
|
||||
try {
|
||||
this.applications = await this.KubernetesApplicationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
}
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.configurations = await this.KubernetesConfigurationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshApplications(namespace) {
|
||||
return this.$async(this.refreshApplicationsAsync, namespace);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.applications = await this.KubernetesApplicationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshVolumes(namespace) {
|
||||
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
||||
const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace;
|
||||
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
||||
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
|
||||
return isSameNamespace && (isUnused || isRWX);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const volumes = await this.KubernetesVolumeService.get(namespace);
|
||||
_.forEach(volumes, (volume) => {
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, this.applications);
|
||||
});
|
||||
this.volumes = volumes;
|
||||
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
||||
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
||||
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.includes(volume.PersistentVolumeClaim.StorageClass.AccessModes, 'RWX');
|
||||
return isUnused || isRWX;
|
||||
});
|
||||
this.availableVolumes = filteredVolumes;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve volumes');
|
||||
}
|
||||
});
|
||||
this.availableVolumes = filteredVolumes;
|
||||
}
|
||||
|
||||
refreshIngresses(namespace) {
|
||||
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||
if (!this.publishViaIngressEnabled()) {
|
||||
this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
||||
if (this.savedFormValues) {
|
||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||
} else {
|
||||
this.formValues.PublishingType = this.ApplicationPublishingTypes.INTERNAL;
|
||||
}
|
||||
}
|
||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||
}
|
||||
|
||||
async refreshNamespaceDataAsync(namespace) {
|
||||
await Promise.all([
|
||||
this.refreshStacks(namespace),
|
||||
this.refreshConfigurations(namespace),
|
||||
this.refreshApplications(namespace),
|
||||
this.refreshIngresses(namespace),
|
||||
this.refreshVolumes(namespace),
|
||||
]);
|
||||
this.onChangeName();
|
||||
}
|
||||
|
||||
refreshNamespaceData(namespace) {
|
||||
return this.$async(this.refreshNamespaceDataAsync, namespace);
|
||||
return this.$async(async () => {
|
||||
await Promise.all([
|
||||
this.refreshStacks(namespace),
|
||||
this.refreshConfigurations(namespace),
|
||||
this.refreshApplications(namespace),
|
||||
this.refreshIngresses(namespace),
|
||||
this.refreshVolumes(namespace),
|
||||
]);
|
||||
this.onChangeName();
|
||||
});
|
||||
}
|
||||
|
||||
resetFormValues() {
|
||||
@@ -768,10 +803,12 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
|
||||
onResourcePoolSelectionChange() {
|
||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||
this.updateSliders();
|
||||
this.refreshNamespaceData(namespace);
|
||||
this.resetFormValues();
|
||||
return this.$async(async () => {
|
||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||
this.updateSliders();
|
||||
await this.refreshNamespaceData(namespace);
|
||||
this.resetFormValues();
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -818,154 +855,143 @@ class KubernetesCreateApplicationController {
|
||||
/* #endregion */
|
||||
|
||||
/* #region APPLICATION - used on edit context only */
|
||||
async getApplicationAsync() {
|
||||
try {
|
||||
const namespace = this.state.params.namespace;
|
||||
[this.application, this.persistentVolumeClaims] = await Promise.all([
|
||||
this.KubernetesApplicationService.get(namespace, this.state.params.name),
|
||||
this.KubernetesPersistentVolumeClaimService.get(namespace),
|
||||
]);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
return this.$async(this.getApplicationAsync);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const namespace = this.state.params.namespace;
|
||||
[this.application, this.persistentVolumeClaims] = await Promise.all([
|
||||
this.KubernetesApplicationService.get(namespace, this.state.params.name),
|
||||
this.KubernetesPersistentVolumeClaimService.get(namespace),
|
||||
]);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
}
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
try {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
useLoadBalancer: false,
|
||||
useServerMetrics: false,
|
||||
sliders: {
|
||||
cpu: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
memory: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
nodes: {
|
||||
memory: 0,
|
||||
cpu: 0,
|
||||
},
|
||||
resourcePoolHasQuota: false,
|
||||
viewReady: false,
|
||||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||
alreadyExists: false,
|
||||
duplicates: {
|
||||
environmentVariables: new KubernetesFormValueDuplicate(),
|
||||
persistedFolders: new KubernetesFormValueDuplicate(),
|
||||
configurationPaths: new KubernetesFormValueDuplicate(),
|
||||
existingVolumes: new KubernetesFormValueDuplicate(),
|
||||
publishedPorts: {
|
||||
containerPorts: new KubernetesFormValueDuplicate(),
|
||||
nodePorts: new KubernetesFormValueDuplicate(),
|
||||
ingressRoutes: new KubernetesFormValueDuplicate(),
|
||||
loadBalancerPorts: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
placements: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
isEdit: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
PersistedFoldersUseExistingVolumes: false,
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
this.editChanges = [];
|
||||
|
||||
if (this.$transition$.params().namespace && this.$transition$.params().name) {
|
||||
this.state.isEdit = true;
|
||||
}
|
||||
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
this.endpoint = endpoint;
|
||||
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
|
||||
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
|
||||
this.formValues = new KubernetesApplicationFormValues();
|
||||
|
||||
const [resourcePools, nodes, ingresses] = await Promise.all([
|
||||
this.KubernetesResourcePoolService.get(),
|
||||
this.KubernetesNodeService.get(),
|
||||
this.KubernetesIngressService.get(),
|
||||
]);
|
||||
this.ingresses = ingresses;
|
||||
|
||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
|
||||
// TODO: refactor @Max
|
||||
// Don't pull all volumes and applications across all namespaces
|
||||
// Use refreshNamespaceData flow (triggered on Init + on Namespace change)
|
||||
// and query only accross the selected namespace
|
||||
if (this.storageClassAvailable()) {
|
||||
const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]);
|
||||
this.volumes = volumes;
|
||||
_.forEach(this.volumes, (volume) => {
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||
});
|
||||
}
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||
this.state.nodes.cpu += item.CPU;
|
||||
});
|
||||
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
||||
|
||||
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||
await this.refreshNamespaceData(namespace);
|
||||
|
||||
if (this.state.isEdit) {
|
||||
await this.getApplication();
|
||||
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
|
||||
this.application,
|
||||
this.resourcePools,
|
||||
this.configurations,
|
||||
this.persistentVolumeClaims,
|
||||
this.nodesLabels
|
||||
);
|
||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
delete this.formValues.ApplicationType;
|
||||
|
||||
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
||||
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||
const volume = _.find(this.availableVolumes, (vol) => vol.PersistentVolumeClaim.Name === persistedFolder.PersistentVolumeClaimName);
|
||||
if (volume) {
|
||||
persistedFolder.UseNewVolume = false;
|
||||
persistedFolder.ExistingVolume = volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
||||
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
|
||||
}
|
||||
|
||||
await this.updateSliders();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
useLoadBalancer: false,
|
||||
useServerMetrics: false,
|
||||
sliders: {
|
||||
cpu: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
memory: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
nodes: {
|
||||
memory: 0,
|
||||
cpu: 0,
|
||||
},
|
||||
resourcePoolHasQuota: false,
|
||||
viewReady: false,
|
||||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||
alreadyExists: false,
|
||||
duplicates: {
|
||||
environmentVariables: new KubernetesFormValidationReferences(),
|
||||
persistedFolders: new KubernetesFormValidationReferences(),
|
||||
configurationPaths: new KubernetesFormValidationReferences(),
|
||||
existingVolumes: new KubernetesFormValidationReferences(),
|
||||
publishedPorts: {
|
||||
containerPorts: new KubernetesFormValidationReferences(),
|
||||
nodePorts: new KubernetesFormValidationReferences(),
|
||||
ingressRoutes: new KubernetesFormValidationReferences(),
|
||||
loadBalancerPorts: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
placements: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
isEdit: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
persistedFoldersUseExistingVolumes: false,
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
this.editChanges = [];
|
||||
|
||||
if (this.state.params.namespace && this.state.params.name) {
|
||||
this.state.isEdit = true;
|
||||
}
|
||||
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
this.endpoint = endpoint;
|
||||
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
|
||||
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
|
||||
this.formValues = new KubernetesApplicationFormValues();
|
||||
|
||||
const [resourcePools, nodes, ingresses] = await Promise.all([
|
||||
this.KubernetesResourcePoolService.get(),
|
||||
this.KubernetesNodeService.get(),
|
||||
this.KubernetesIngressService.get(),
|
||||
]);
|
||||
this.ingresses = ingresses;
|
||||
|
||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
if (!this.formValues.ResourcePool) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||
this.state.nodes.cpu += item.CPU;
|
||||
});
|
||||
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
||||
|
||||
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||
await this.refreshNamespaceData(namespace);
|
||||
|
||||
if (this.state.isEdit) {
|
||||
await this.getApplication();
|
||||
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
|
||||
this.application,
|
||||
this.resourcePools,
|
||||
this.configurations,
|
||||
this.persistentVolumeClaims,
|
||||
this.nodesLabels
|
||||
);
|
||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
delete this.formValues.ApplicationType;
|
||||
|
||||
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
||||
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.PersistentVolumeClaimName]);
|
||||
if (volume) {
|
||||
persistedFolder.UseNewVolume = false;
|
||||
persistedFolder.ExistingVolume = volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
await this.refreshNamespaceData(namespace);
|
||||
} else {
|
||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
||||
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
|
||||
}
|
||||
|
||||
this.updateSliders();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
}
|
||||
|
||||
|
||||
@@ -190,14 +190,33 @@
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div ng-if="!ctrl.isExternalApplication() && !ctrl.isSystemNamespace()" style="margin-bottom: 15px;">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.application.edit" style="margin-left: 0;">
|
||||
<div ng-if="!ctrl.isSystemNamespace()">
|
||||
<button
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ui-sref="kubernetes.applications.application.edit"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
>
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" style="margin-left: 0;" ng-click="ctrl.redeployApplication()">
|
||||
<button
|
||||
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
ng-click="ctrl.redeployApplication()"
|
||||
>
|
||||
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" style="margin-left: 0;" ng-click="ctrl.rollbackApplication()" ng-disabled="ctrl.application.Revisions.length < 2">
|
||||
<button
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
ng-click="ctrl.rollbackApplication()"
|
||||
ng-disabled="ctrl.application.Revisions.length < 2"
|
||||
>
|
||||
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import angular from 'angular';
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
@@ -67,8 +67,8 @@ function computeAffinities(nodes, application) {
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key]) > parseInt(e.values[0])) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key]) < parseInt(e.values[0]))
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key], 10) > parseInt(e.values[0], 10)) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key], 10) < parseInt(e.values[0], 10))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
id="resource-pool-selector"
|
||||
ng-model="ctrl.formValues.ResourcePool"
|
||||
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
|
||||
ng-change="ctrl.onResourcePoolSelectionChange()"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
@@ -82,7 +82,7 @@ class KubernetesConfigureController {
|
||||
const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
onChangeIngressClassName(index) {
|
||||
@@ -212,7 +212,7 @@ class KubernetesConfigureController {
|
||||
viewReady: false,
|
||||
endpointId: this.$stateParams.id,
|
||||
duplicates: {
|
||||
ingressClasses: new KubernetesFormValueDuplicate(),
|
||||
ingressClasses: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
|
||||
class KubernetesCreateResourcePoolController {
|
||||
@@ -44,7 +44,7 @@ class KubernetesCreateResourcePoolController {
|
||||
}
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
@@ -58,7 +58,7 @@ class KubernetesCreateResourcePoolController {
|
||||
/* #endregion */
|
||||
|
||||
isCreateButtonDisabled() {
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasDuplicates;
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasRefs;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
@@ -109,13 +109,10 @@ class KubernetesCreateResourcePoolController {
|
||||
|
||||
/* #region GET INGRESSES */
|
||||
async getIngressesAsync() {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
|
||||
} finally {
|
||||
this.state.ingressesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +151,7 @@ class KubernetesCreateResourcePoolController {
|
||||
isAlreadyExist: false,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValueDuplicate(),
|
||||
ingressHosts: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,17 @@
|
||||
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.HasQuota">
|
||||
<kubernetes-resource-reservation
|
||||
ng-if="ctrl.pool.Quota"
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
||||
cpu="ctrl.state.cpuUsed"
|
||||
memory="ctrl.state.memoryUsed"
|
||||
cpu-limit="ctrl.formValues.CpuLimit"
|
||||
memory-limit="ctrl.formValues.MemoryLimit"
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</div>
|
||||
<!-- !quotas-switch -->
|
||||
<div ng-if="ctrl.formValues.HasQuota && ctrl.isAdmin && ctrl.isEditable">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -50,7 +61,7 @@
|
||||
<div class="col-sm-3">
|
||||
<slider
|
||||
model="ctrl.formValues.MemoryLimit"
|
||||
floor="ctrl.defaults.MemoryLimit"
|
||||
floor="ctrl.ResourceQuotaDefaults.MemoryLimit"
|
||||
ceil="ctrl.state.sliderMaxMemory"
|
||||
step="128"
|
||||
ng-if="ctrl.state.sliderMaxMemory"
|
||||
@@ -60,7 +71,7 @@
|
||||
<input
|
||||
name="memory_limit"
|
||||
type="number"
|
||||
min="{{ ctrl.defaults.MemoryLimit }}"
|
||||
min="{{ ctrl.ResourceQuotaDefaults.MemoryLimit }}"
|
||||
max="{{ ctrl.state.sliderMaxMemory }}"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.MemoryLimit"
|
||||
@@ -78,7 +89,7 @@
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolEditForm.pool_name.$error">
|
||||
<p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.ResourceQuotaDefaults.MemoryLimit }} and
|
||||
{{ ctrl.state.sliderMaxMemory }}</p
|
||||
>
|
||||
</div>
|
||||
@@ -93,7 +104,7 @@
|
||||
<div class="col-sm-5">
|
||||
<slider
|
||||
model="ctrl.formValues.CpuLimit"
|
||||
floor="ctrl.defaults.CpuLimit"
|
||||
floor="ctrl.ResourceQuotaDefaults.CpuLimit"
|
||||
ceil="ctrl.state.sliderMaxCpu"
|
||||
step="0.1"
|
||||
precision="2"
|
||||
@@ -109,17 +120,31 @@
|
||||
<!-- !cpu-limit-input -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.HasQuota">
|
||||
<kubernetes-resource-reservation
|
||||
ng-if="ctrl.pool.Quota"
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
||||
cpu="ctrl.state.cpuUsed"
|
||||
memory="ctrl.state.memoryUsed"
|
||||
cpu-limit="ctrl.formValues.CpuLimit"
|
||||
memory-limit="ctrl.formValues.MemoryLimit"
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
<!-- #region LOADBALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
|
||||
of load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
@@ -250,33 +275,7 @@
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Load balancers
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
|
||||
of load balancers in this resource pool.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region LOAD-BALANCERS -->
|
||||
<!-- #region STORAGES -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Storages
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import angular from 'angular';
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||
|
||||
class KubernetesResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
@@ -28,7 +29,8 @@ class KubernetesResourcePoolController {
|
||||
KubernetesPodService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesNamespaceHelper,
|
||||
KubernetesIngressService
|
||||
KubernetesIngressService,
|
||||
KubernetesVolumeService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
@@ -46,18 +48,17 @@ class KubernetesResourcePoolController {
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||
|
||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
|
||||
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
this.getApplications = this.getApplications.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
this.getIngresses = this.getIngresses.bind(this);
|
||||
this.getIngressesAsync = this.getIngressesAsync.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -74,7 +75,7 @@ class KubernetesResourcePoolController {
|
||||
}
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
@@ -92,7 +93,7 @@ class KubernetesResourcePoolController {
|
||||
}
|
||||
|
||||
isUpdateButtonDisabled() {
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasDuplicates;
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasRefs;
|
||||
}
|
||||
|
||||
isQuotaValid() {
|
||||
@@ -107,11 +108,11 @@ class KubernetesResourcePoolController {
|
||||
}
|
||||
|
||||
checkDefaults() {
|
||||
if (this.formValues.CpuLimit < this.defaults.CpuLimit) {
|
||||
this.formValues.CpuLimit = this.defaults.CpuLimit;
|
||||
if (this.formValues.CpuLimit < KubernetesResourceQuotaDefaults.CpuLimit) {
|
||||
this.formValues.CpuLimit = KubernetesResourceQuotaDefaults.CpuLimit;
|
||||
}
|
||||
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) {
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit);
|
||||
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit)) {
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,45 +141,12 @@ class KubernetesResourcePoolController {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* #region UPDATE RESOURCE POOL */
|
||||
async updateResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.checkDefaults();
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
const cpuLimit = this.formValues.CpuLimit;
|
||||
const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||
const owner = this.pool.Namespace.ResourcePoolOwner;
|
||||
const quota = this.pool.Quota;
|
||||
|
||||
if (this.formValues.HasQuota) {
|
||||
if (quota) {
|
||||
quota.CpuLimit = cpuLimit;
|
||||
quota.MemoryLimit = memoryLimit;
|
||||
await this.KubernetesResourceQuotaService.update(quota);
|
||||
} else {
|
||||
await this.createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit);
|
||||
}
|
||||
} else if (quota) {
|
||||
await this.KubernetesResourceQuotaService.delete(quota);
|
||||
}
|
||||
|
||||
const promises = _.map(this.formValues.IngressClasses, (c) => {
|
||||
c.Namespace = namespace;
|
||||
if (c.WasSelected === false && c.Selected === true) {
|
||||
const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
return this.KubernetesIngressService.create(ingress);
|
||||
} else if (c.WasSelected === true && c.Selected === false) {
|
||||
return this.KubernetesIngressService.delete(c);
|
||||
} else if (c.WasSelected === true && c.Selected === true) {
|
||||
const oldIngress = _.find(this.ingresses, { Name: c.IngressClass.Name });
|
||||
const newIngress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
newIngress.Paths = angular.copy(oldIngress.Paths);
|
||||
newIngress.PreviousHost = oldIngress.Host;
|
||||
return this.KubernetesIngressService.patch(oldIngress, newIngress);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues);
|
||||
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
@@ -212,75 +180,70 @@ class KubernetesResourcePoolController {
|
||||
return this.$async(this.updateResourcePoolAsync);
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
/* #region GET EVENTS */
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET APPLICATIONS */
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name);
|
||||
this.applications = _.map(this.applications, (app) => {
|
||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
||||
app.CPU = resourceReservation.CPU;
|
||||
app.Memory = resourceReservation.Memory;
|
||||
return app;
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications.');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name);
|
||||
this.applications = _.map(this.applications, (app) => {
|
||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
||||
app.CPU = resourceReservation.CPU;
|
||||
app.Memory = resourceReservation.Memory;
|
||||
return app;
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications.');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET INGRESSES */
|
||||
async getIngressesAsync() {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
|
||||
_.forEach(this.ingresses, (ing) => {
|
||||
ing.Namespace = namespace;
|
||||
_.forEach(ing.Paths, (path) => {
|
||||
const application = _.find(this.applications, { ServiceName: path.ServiceName });
|
||||
path.ApplicationName = application && application.Name ? application.Name : '-';
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
|
||||
} finally {
|
||||
this.state.ingressesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getIngresses() {
|
||||
return this.$async(this.getIngressesAsync);
|
||||
return this.$async(async () => {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
|
||||
_.forEach(this.ingresses, (ing) => {
|
||||
ing.Namespace = namespace;
|
||||
_.forEach(ing.Paths, (path) => {
|
||||
const application = _.find(this.applications, { ServiceName: path.ServiceName });
|
||||
path.ApplicationName = application && application.Name ? application.Name : '-';
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
|
||||
} finally {
|
||||
this.state.ingressesLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -290,9 +253,6 @@ class KubernetesResourcePoolController {
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
this.endpoint = endpoint;
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
this.defaults = KubernetesResourceQuotaDefaults;
|
||||
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
|
||||
this.formValues.HasQuota = false;
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
@@ -312,7 +272,7 @@ class KubernetesResourcePoolController {
|
||||
eventWarningCount: 0,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValueDuplicate(),
|
||||
ingressHosts: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -320,9 +280,11 @@ class KubernetesResourcePoolController {
|
||||
|
||||
const name = this.$transition$.params().id;
|
||||
|
||||
const [nodes, pool] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get(name)]);
|
||||
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
|
||||
|
||||
this.pool = pool;
|
||||
this.pool = _.find(pools, { Namespace: { Name: name } });
|
||||
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
||||
this.formValues.Name = this.pool.Namespace.Name;
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
||||
@@ -330,13 +292,10 @@ class KubernetesResourcePoolController {
|
||||
});
|
||||
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
||||
|
||||
const quota = pool.Quota;
|
||||
const quota = this.pool.Quota;
|
||||
if (quota) {
|
||||
this.oldQuota = angular.copy(quota);
|
||||
this.formValues.HasQuota = true;
|
||||
this.formValues.CpuLimit = quota.CpuLimit;
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
|
||||
|
||||
this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
|
||||
this.state.cpuUsed = quota.CpuLimitUsed;
|
||||
this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
|
||||
}
|
||||
@@ -354,6 +313,7 @@ class KubernetesResourcePoolController {
|
||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
|
||||
}
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
ng-model="ctrl.state.volumeSize"
|
||||
placeholder="20"
|
||||
ng-min="0"
|
||||
min="0"
|
||||
ng-change="ctrl.onChangeSize()"
|
||||
required
|
||||
/>
|
||||
@@ -100,11 +101,11 @@
|
||||
</div>
|
||||
|
||||
<div class="form-inline">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-show="ctrl.state.volumeSizeError || kubernetesVolumeUpdateForm.size.$invalid">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-show="ctrl.state.errors.volumeSize || kubernetesVolumeUpdateForm.size.$invalid">
|
||||
<div ng-messages="kubernetesVolumeUpdateForm.size.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
<p ng-show="ctrl.state.volumeSizeError && !kubernetesVolumeUpdateForm.size.$invalid"
|
||||
<p ng-show="ctrl.state.errors.volumeSize && !kubernetesVolumeUpdateForm.size.$invalid"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> The new size must be greater than the actual size.</p
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -64,18 +64,17 @@ class KubernetesVolumeController {
|
||||
|
||||
onChangeSize() {
|
||||
if (this.state.volumeSize) {
|
||||
const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit);
|
||||
const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 });
|
||||
if (this.state.oldVolumeSize > size) {
|
||||
this.state.volumeSizeError = true;
|
||||
this.state.errors.volumeSize = true;
|
||||
} else {
|
||||
this.volume.PersistentVolumeClaim.Storage = size;
|
||||
this.state.volumeSizeError = false;
|
||||
this.state.errors.volumeSize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizeIsValid() {
|
||||
return !this.state.volumeSizeError && this.state.oldVolumeSize !== this.volume.PersistentVolumeClaim.Storage;
|
||||
return !this.state.errors.volumeSize && this.state.volumeSize && this.state.oldVolumeSize !== filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +83,7 @@ class KubernetesVolumeController {
|
||||
|
||||
async updateVolumeAsync(redeploy) {
|
||||
try {
|
||||
this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0) + 'i';
|
||||
this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0);
|
||||
await this.KubernetesPersistentVolumeClaimService.patch(this.oldVolume.PersistentVolumeClaim, this.volume.PersistentVolumeClaim);
|
||||
this.Notifications.success('Volume successfully updated');
|
||||
|
||||
@@ -126,9 +125,9 @@ class KubernetesVolumeController {
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||
this.volume = volume;
|
||||
this.oldVolume = angular.copy(volume);
|
||||
this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2));
|
||||
this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2), 10);
|
||||
this.state.volumeSizeUnit = volume.PersistentVolumeClaim.Storage.slice(-2);
|
||||
this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage);
|
||||
this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage, { base: 10 });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve volume');
|
||||
}
|
||||
@@ -179,9 +178,11 @@ class KubernetesVolumeController {
|
||||
increaseSize: false,
|
||||
volumeSize: 0,
|
||||
volumeSizeUnit: 'GB',
|
||||
volumeSizeError: false,
|
||||
volumeSharedAccessPolicy: '',
|
||||
volumeSharedAccessPolicyTooltip: '',
|
||||
errors: {
|
||||
volumeSize: false,
|
||||
},
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
require('../../templates/advancedDeploymentPanel.html');
|
||||
|
||||
import * as _ from 'lodash-es';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import angular from 'angular';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||
|
||||
function buildStorages(storages, volumes) {
|
||||
_.forEach(storages, (s) => {
|
||||
@@ -15,28 +16,9 @@ function buildStorages(storages, volumes) {
|
||||
}
|
||||
|
||||
function computeSize(volumes) {
|
||||
let hasT,
|
||||
hasG,
|
||||
hasM = false;
|
||||
const size = _.sumBy(volumes, (v) => {
|
||||
const storage = v.PersistentVolumeClaim.Storage;
|
||||
if (!hasT && _.endsWith(storage, 'TB')) {
|
||||
hasT = true;
|
||||
} else if (!hasG && _.endsWith(storage, 'GB')) {
|
||||
hasG = true;
|
||||
} else if (!hasM && _.endsWith(storage, 'MB')) {
|
||||
hasM = true;
|
||||
}
|
||||
return filesizeParser(storage, { base: 10 });
|
||||
});
|
||||
if (hasT) {
|
||||
return size / 1000 / 1000 / 1000 / 1000 + 'TB';
|
||||
} else if (hasG) {
|
||||
return size / 1000 / 1000 / 1000 + 'GB';
|
||||
} else if (hasM) {
|
||||
return size / 1000 / 1000 + 'MB';
|
||||
}
|
||||
return size;
|
||||
const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 }));
|
||||
const format = KubernetesResourceQuotaHelper.formatBytes(size);
|
||||
return `${format.Size}${format.SizeUnit}`;
|
||||
}
|
||||
|
||||
class KubernetesVolumesController {
|
||||
|
||||
@@ -26,8 +26,24 @@ class EndpointItemController {
|
||||
return _.join(tagNames, ',');
|
||||
}
|
||||
|
||||
isEdgeEndpoint() {
|
||||
return this.model.Type === 4 || this.model.Type === 7;
|
||||
}
|
||||
|
||||
calcIsCheckInValid() {
|
||||
if (!this.isEdgeEndpoint()) {
|
||||
return false;
|
||||
}
|
||||
const checkInInterval = this.model.EdgeCheckinInterval;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// give checkIn some wiggle room
|
||||
return now - this.model.LastCheckInDate <= checkInInterval * 2;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.endpointTags = this.joinTags();
|
||||
this.isCheckInValid = this.calcIsCheckInValid();
|
||||
}
|
||||
|
||||
$onChanges({ tags, model }) {
|
||||
@@ -35,6 +51,10 @@ class EndpointItemController {
|
||||
return;
|
||||
}
|
||||
this.endpointTags = this.joinTags();
|
||||
|
||||
if (model) {
|
||||
this.isCheckInValid = this.calcIsCheckInValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,18 +19,26 @@
|
||||
{{ $ctrl.model.Name }}
|
||||
</span>
|
||||
<span class="space-left blocklist-item-subtitle">
|
||||
<span ng-if="$ctrl.model.Type === 4 || $ctrl.model.Type === 7" class="small text-muted">
|
||||
<span ng-if="$ctrl.model.EdgeID"><i class="fas fa-link"></i> associated</span>
|
||||
<span ng-if="!$ctrl.model.EdgeID"><i class="fas fa-unlink"></i> <s>associated</s></span>
|
||||
<span ng-if="$ctrl.isEdgeEndpoint()">
|
||||
<span ng-if="!$ctrl.model.EdgeID" class="label label-default"><s>associated</s></span>
|
||||
<span ng-if="$ctrl.model.EdgeID">
|
||||
<span class="label" ng-class="{ 'label-danger': !$ctrl.isCheckInValid, 'label-success': $ctrl.isCheckInValid }">heartbeat</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.LastCheckInDate">
|
||||
{{ $ctrl.model.LastCheckInDate | getisodatefromtimestamp }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="label label-{{ $ctrl.model.Status | endpointstatusbadge }}" ng-if="$ctrl.model.Type !== 4 && $ctrl.model.Type !== 7">
|
||||
{{ $ctrl.model.Status === 1 ? 'up' : 'down' }}
|
||||
</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Snapshots[0]">
|
||||
{{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Kubernetes.Snapshots[0]">
|
||||
{{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
|
||||
<span ng-if="!$ctrl.isEdgeEndpoint()">
|
||||
<span class="label label-{{ $ctrl.model.Status | endpointstatusbadge }}">
|
||||
{{ $ctrl.model.Status === 1 ? 'up' : 'down' }}
|
||||
</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Snapshots[0]">
|
||||
{{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Kubernetes.Snapshots[0]">
|
||||
{{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { clear as clearSessionStorage } from './session-storage';
|
||||
|
||||
angular.module('portainer.app').factory('Authentication', [
|
||||
'$async',
|
||||
'$state',
|
||||
@@ -38,6 +40,7 @@ angular.module('portainer.app').factory('Authentication', [
|
||||
await Auth.logout().$promise;
|
||||
}
|
||||
|
||||
clearSessionStorage();
|
||||
StateManager.clean();
|
||||
EndpointProvider.clean();
|
||||
LocalStorage.cleanAuthData();
|
||||
|
||||
@@ -1,70 +1,88 @@
|
||||
angular.module('portainer.app').factory('DatatableService', [
|
||||
'LocalStorage',
|
||||
function DatatableServiceFactory(LocalStorage) {
|
||||
'use strict';
|
||||
import angular from 'angular';
|
||||
|
||||
var service = {};
|
||||
import * as sessionStorage from './session-storage';
|
||||
|
||||
service.setDataTableSettings = function (key, settings) {
|
||||
LocalStorage.storeDataTableSettings(key, settings);
|
||||
angular.module('portainer.app').factory('DatatableService', DatatableServiceFactory);
|
||||
|
||||
const DATATABLE_PREFIX = 'datatable_';
|
||||
const TEXT_FILTER_KEY_PREFIX = `${DATATABLE_PREFIX}text_filter_`;
|
||||
|
||||
/* @ngInject */
|
||||
function DatatableServiceFactory(LocalStorage) {
|
||||
return {
|
||||
setDataTableSettings,
|
||||
getDataTableSettings,
|
||||
setDataTableTextFilters,
|
||||
getDataTableTextFilters,
|
||||
setDataTableFilters,
|
||||
getDataTableFilters,
|
||||
getDataTableOrder,
|
||||
setDataTableOrder,
|
||||
setDataTableExpandedItems,
|
||||
setColumnVisibilitySettings,
|
||||
getDataTableExpandedItems,
|
||||
setDataTableSelectedItems,
|
||||
getDataTableSelectedItems,
|
||||
getColumnVisibilitySettings,
|
||||
};
|
||||
|
||||
function setDataTableSettings(key, settings) {
|
||||
LocalStorage.storeDataTableSettings(key, settings);
|
||||
}
|
||||
|
||||
function getDataTableSettings(key) {
|
||||
return LocalStorage.getDataTableSettings(key);
|
||||
}
|
||||
|
||||
function setDataTableTextFilters(key, filters) {
|
||||
sessionStorage.save(TEXT_FILTER_KEY_PREFIX + key, filters);
|
||||
}
|
||||
|
||||
function getDataTableTextFilters(key) {
|
||||
return sessionStorage.get(TEXT_FILTER_KEY_PREFIX + key);
|
||||
}
|
||||
|
||||
function setDataTableFilters(key, filters) {
|
||||
LocalStorage.storeDataTableFilters(key, filters);
|
||||
}
|
||||
|
||||
function getDataTableFilters(key) {
|
||||
return LocalStorage.getDataTableFilters(key);
|
||||
}
|
||||
|
||||
function getDataTableOrder(key) {
|
||||
return LocalStorage.getDataTableOrder(key);
|
||||
}
|
||||
|
||||
function setDataTableOrder(key, orderBy, reverse) {
|
||||
var filter = {
|
||||
orderBy: orderBy,
|
||||
reverse: reverse,
|
||||
};
|
||||
LocalStorage.storeDataTableOrder(key, filter);
|
||||
}
|
||||
|
||||
service.getDataTableSettings = function (key) {
|
||||
return LocalStorage.getDataTableSettings(key);
|
||||
};
|
||||
function setDataTableExpandedItems(key, expandedItems) {
|
||||
LocalStorage.storeDataTableExpandedItems(key, expandedItems);
|
||||
}
|
||||
|
||||
service.setDataTableTextFilters = function (key, filters) {
|
||||
LocalStorage.storeDataTableTextFilters(key, filters);
|
||||
};
|
||||
function setColumnVisibilitySettings(key, columnVisibility) {
|
||||
LocalStorage.storeColumnVisibilitySettings(key, columnVisibility);
|
||||
}
|
||||
|
||||
service.getDataTableTextFilters = function (key) {
|
||||
return LocalStorage.getDataTableTextFilters(key);
|
||||
};
|
||||
function getDataTableExpandedItems(key) {
|
||||
return LocalStorage.getDataTableExpandedItems(key);
|
||||
}
|
||||
|
||||
service.setDataTableFilters = function (key, filters) {
|
||||
LocalStorage.storeDataTableFilters(key, filters);
|
||||
};
|
||||
function setDataTableSelectedItems(key, selectedItems) {
|
||||
LocalStorage.storeDataTableSelectedItems(key, selectedItems);
|
||||
}
|
||||
|
||||
service.getDataTableFilters = function (key) {
|
||||
return LocalStorage.getDataTableFilters(key);
|
||||
};
|
||||
function getDataTableSelectedItems(key) {
|
||||
return LocalStorage.getDataTableSelectedItems(key);
|
||||
}
|
||||
|
||||
service.getDataTableOrder = function (key) {
|
||||
return LocalStorage.getDataTableOrder(key);
|
||||
};
|
||||
|
||||
service.setDataTableOrder = function (key, orderBy, reverse) {
|
||||
var filter = {
|
||||
orderBy: orderBy,
|
||||
reverse: reverse,
|
||||
};
|
||||
LocalStorage.storeDataTableOrder(key, filter);
|
||||
};
|
||||
|
||||
service.setDataTableExpandedItems = function (key, expandedItems) {
|
||||
LocalStorage.storeDataTableExpandedItems(key, expandedItems);
|
||||
};
|
||||
|
||||
service.setColumnVisibilitySettings = function (key, columnVisibility) {
|
||||
LocalStorage.storeColumnVisibilitySettings(key, columnVisibility);
|
||||
};
|
||||
|
||||
service.getDataTableExpandedItems = function (key) {
|
||||
return LocalStorage.getDataTableExpandedItems(key);
|
||||
};
|
||||
|
||||
service.setDataTableSelectedItems = function (key, selectedItems) {
|
||||
LocalStorage.storeDataTableSelectedItems(key, selectedItems);
|
||||
};
|
||||
|
||||
service.getDataTableSelectedItems = function (key) {
|
||||
return LocalStorage.getDataTableSelectedItems(key);
|
||||
};
|
||||
|
||||
service.getColumnVisibilitySettings = function (key) {
|
||||
return LocalStorage.getColumnVisibilitySettings(key);
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
function getColumnVisibilitySettings(key) {
|
||||
return LocalStorage.getColumnVisibilitySettings(key);
|
||||
}
|
||||
}
|
||||
|
||||
31
app/portainer/services/session-storage.js
Normal file
31
app/portainer/services/session-storage.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* clears the sessionStorage
|
||||
*/
|
||||
export function clear() {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* stores `value` as string in `sessionStorage[key]`
|
||||
*
|
||||
* @param {string} key the key to store value at
|
||||
* @param {any} value the value to store - will be stringified using JSON.stringify
|
||||
*
|
||||
*/
|
||||
export function save(key, value) {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* get parses the value stored in sessionStorage[key], if it's not available returns undefined
|
||||
*
|
||||
* @param {string} key
|
||||
*/
|
||||
export function get(key) {
|
||||
try {
|
||||
const value = sessionStorage.getItem(key);
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ angular
|
||||
EndpointProvider,
|
||||
StateManager,
|
||||
ModalService,
|
||||
MotdService
|
||||
MotdService,
|
||||
SettingsService
|
||||
) {
|
||||
$scope.state = {
|
||||
connectingToEdgeEndpoint: false,
|
||||
@@ -82,7 +83,7 @@ angular
|
||||
var groups = data.groups;
|
||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||
EndpointProvider.setEndpoints(endpoints);
|
||||
deferred.resolve({ endpoints: endpoints, totalCount: data.endpoints.totalCount });
|
||||
deferred.resolve({ endpoints: decorateEndpoints(endpoints), totalCount: data.endpoints.totalCount });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||
@@ -98,14 +99,15 @@ angular
|
||||
});
|
||||
|
||||
try {
|
||||
const [{ totalCount, endpoints }, tags] = await Promise.all([getPaginatedEndpoints(0, 100), TagService.tags()]);
|
||||
const [{ totalCount, endpoints }, tags, settings] = await Promise.all([getPaginatedEndpoints(0, 100), TagService.tags(), SettingsService.settings()]);
|
||||
$scope.tags = tags;
|
||||
$scope.defaultEdgeCheckInInterval = settings.EdgeAgentCheckinInterval;
|
||||
|
||||
$scope.totalCount = totalCount;
|
||||
if (totalCount > 100) {
|
||||
$scope.endpoints = [];
|
||||
} else {
|
||||
$scope.endpoints = endpoints;
|
||||
$scope.endpoints = decorateEndpoints(endpoints);
|
||||
}
|
||||
} catch (err) {
|
||||
Notifications.error('Failed loading page data', err);
|
||||
@@ -113,4 +115,10 @@ angular
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
function decorateEndpoints(endpoints) {
|
||||
return endpoints.map((endpoint) => {
|
||||
return { ...endpoint, EdgeCheckinInterval: endpoint.EdgeAgentCheckinInterval || $scope.defaultEdgeCheckInInterval };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,5 +225,6 @@
|
||||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel ng-if="stack" resource-id="stack.Name" resource-control="stack.ResourceControl" resource-type="'stack'"> </por-access-control-panel>
|
||||
<por-access-control-panel ng-if="stack" resource-id="stack.EndpointId + '_' + stack.Name" resource-control="stack.ResourceControl" resource-type="'stack'">
|
||||
</por-access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
||||
@@ -254,7 +254,7 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
this.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
|
||||
$scope.state.provider = endpointMode.provider === 'DOCKER_STANDALONE' ? 2 : 1;
|
||||
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"start:server": "grunt clean:server && grunt start:server",
|
||||
"start:client": "grunt clean:client && grunt start:client",
|
||||
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
|
||||
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
|
||||
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
|
||||
"start:toolkit": "grunt start:toolkit",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
|
||||
@@ -60,6 +60,15 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, '.tmp'),
|
||||
compress: true,
|
||||
port: 8999,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:9000',
|
||||
},
|
||||
open: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const path = require('path');
|
||||
const webpackMerge = require('webpack-merge');
|
||||
const commonConfig = require('./webpack.common.js');
|
||||
|
||||
@@ -18,13 +17,4 @@ module.exports = webpackMerge(commonConfig, {
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, '.tmp'),
|
||||
compress: true,
|
||||
port: 8999,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:9000',
|
||||
},
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user