Compare commits
143 Commits
release/2.
...
refactor/E
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
733013e484 | ||
|
|
6c7b8f87a9 | ||
|
|
690f6e8af3 | ||
|
|
abcf73a415 | ||
|
|
ff7847aaa5 | ||
|
|
a89c3773dd | ||
|
|
5d75ca34ea | ||
|
|
d47a9d590e | ||
|
|
bd679ae806 | ||
|
|
5de7ecb5f0 | ||
|
|
b3cd9c69df | ||
|
|
73311b6f32 | ||
|
|
93ddcfecd9 | ||
|
|
2bffba7371 | ||
|
|
37ca62eb06 | ||
|
|
fa208c7f2a | ||
|
|
6fac3fa127 | ||
|
|
171392c5ca | ||
|
|
d48ff2921b | ||
|
|
3165d354b5 | ||
|
|
9c2dbac479 | ||
|
|
318844226c | ||
|
|
e96f63023e | ||
|
|
1765b99336 | ||
|
|
74a0d4c12e | ||
|
|
3372f78cbf | ||
|
|
fe082f762f | ||
|
|
a8d3cda3fa | ||
|
|
ad7f87122d | ||
|
|
6f6f78fbe5 | ||
|
|
1bb02eea59 | ||
|
|
cf459a2d28 | ||
|
|
7d91ab72e1 | ||
|
|
cb804e8813 | ||
|
|
0973808234 | ||
|
|
edd5193100 | ||
|
|
0ad66510a9 | ||
|
|
5a6cd2002d | ||
|
|
1fbf13e812 | ||
|
|
a9406764ee | ||
|
|
dfb0ba9efe | ||
|
|
df2269a2fe | ||
|
|
8b4a74f06e | ||
|
|
48f2e7316a | ||
|
|
b76bcf0ee7 | ||
|
|
24893573aa | ||
|
|
118809a9c0 | ||
|
|
61be10bb00 | ||
|
|
4bd3f61ce6 | ||
|
|
48c2f127f8 | ||
|
|
b588d901cf | ||
|
|
2c4c638f46 | ||
|
|
3ed92e5fee | ||
|
|
804fdd414e | ||
|
|
661f0aad49 | ||
|
|
58de8e175f | ||
|
|
1e21aeb7e8 | ||
|
|
a79aa221d3 | ||
|
|
50b2f789a3 | ||
|
|
bc70198102 | ||
|
|
1b1a50d6b5 | ||
|
|
34cc8ea96a | ||
|
|
59ec22f706 | ||
|
|
c47e840b37 | ||
|
|
edf048570b | ||
|
|
b71ca2afb0 | ||
|
|
9ff8f42a66 | ||
|
|
125d84cbd1 | ||
|
|
fa798665cd | ||
|
|
95fbf7500c | ||
|
|
584a46d9d4 | ||
|
|
085762a1f4 | ||
|
|
6c32edc5b5 | ||
|
|
389561eb28 | ||
|
|
bc54d687be | ||
|
|
8e45076f35 | ||
|
|
87dda810fc | ||
|
|
4e77d2d772 | ||
|
|
0b62a3d664 | ||
|
|
84f354452b | ||
|
|
c24d8fab0f | ||
|
|
5362e15624 | ||
|
|
07c6ce84c2 | ||
|
|
ecd0eb6170 | ||
|
|
8dbb802fb1 | ||
|
|
07e7fbd270 | ||
|
|
65821aaccc | ||
|
|
d33ac8c588 | ||
|
|
102a07346a | ||
|
|
8fc5a5e8a1 | ||
|
|
cdfa9b25a8 | ||
|
|
e7fc996424 | ||
|
|
1c374b9fd2 | ||
|
|
d9db789511 | ||
|
|
5a3687a564 | ||
|
|
6e53bf5dc7 | ||
|
|
e25141d899 | ||
|
|
4f7b432f44 | ||
|
|
c5fe994cd2 | ||
|
|
c30292cedd | ||
|
|
33a29159d2 | ||
|
|
187b66f5cb | ||
|
|
730fdb160d | ||
|
|
efa125790f | ||
|
|
ac9ca7d5e3 | ||
|
|
f99329eb7e | ||
|
|
b02bf0c9d7 | ||
|
|
7ae5a3042c | ||
|
|
eb9f6c77f4 | ||
|
|
7088da5157 | ||
|
|
da422d6ed6 | ||
|
|
eb517c2e12 | ||
|
|
76916b0ad6 | ||
|
|
19a09b4730 | ||
|
|
8f32517baa | ||
|
|
f864b1bf69 | ||
|
|
e57454cd7c | ||
|
|
b3e04adee3 | ||
|
|
a78d8a4ff1 | ||
|
|
9f5ac154aa | ||
|
|
0627e16b35 | ||
|
|
2a1b8efaed | ||
|
|
98972dec0d | ||
|
|
aa8fc52106 | ||
|
|
5839f96787 | ||
|
|
7cc28b10a0 | ||
|
|
4aea5690a8 | ||
|
|
335f951e6b | ||
|
|
42e782452c | ||
|
|
d2fe76368a | ||
|
|
aa7d7845c1 | ||
|
|
a86c7046df | ||
|
|
ff6185cc81 | ||
|
|
f360392d39 | ||
|
|
fa44a62c4a | ||
|
|
2a384d4c64 | ||
|
|
b6fbf8eecc | ||
|
|
69c17986d9 | ||
|
|
120584909c | ||
|
|
c24dc3112b | ||
|
|
1e80061186 | ||
|
|
c267355759 | ||
|
|
47c1af93ea |
@@ -1,4 +1,5 @@
|
||||
# prettier
|
||||
cf5056d9c03b62d91a25c3b9127caac838695f98
|
||||
|
||||
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)
|
||||
# prettier v2
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
Thanks for opening an issue on Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
|
||||
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
|
||||
|
||||
If you are reporting a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -12,7 +12,7 @@ Thanks for reporting a bug for Portainer !
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
@@ -47,7 +47,7 @@ You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
|
||||
- Browser:
|
||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
|
||||
- Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
**Additional context**
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/Custom.md
vendored
6
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -4,11 +4,11 @@ about: Ask us a question about Portainer usage or deployment
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before you start, we need a little bit more information from you:
|
||||
|
||||
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
|
||||
|
||||
Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
@@ -16,7 +16,7 @@ Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -4,14 +4,13 @@ about: Suggest a feature/enhancement that should be added in Portainer
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for opening a feature request for Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
closes #0 <!-- Github issue number (remove if unknown) -->
|
||||
closes [CE-0] <!-- Jira link number (remove if unknown). Please also add the same [CE-XXX] at the back of the PR title -->
|
||||
|
||||
### Changes:
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
# ESLint and Prettier must be in `package.json`
|
||||
- name: Install Node.js dependencies
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
|
||||
2
.github/workflows/test-client.yaml
vendored
2
.github/workflows/test-client.yaml
vendored
@@ -6,6 +6,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
|
||||
"program": "${workspaceRoot}/api/cmd/portainer",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"env": {},
|
||||
"showLog": true,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast", "-E", "exportloopref"],
|
||||
"gitlens.advanced.blame.customArguments": ["–ignore-revs-file", ".git-blame-ignore-revs"]
|
||||
"gopls": {
|
||||
"build.expandWorkspaceToModule": false
|
||||
},
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
|
||||
|
||||
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
||||
|
||||
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
|
||||
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
|
||||
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
|
||||
|
||||
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
|
||||
|
||||
|
||||
@@ -80,8 +80,7 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
|
||||
}
|
||||
|
||||
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
|
||||
dbFileName := datastore.Connection().GetDatabaseFileName()
|
||||
backupWriter, err := os.Create(filepath.Join(backupDirPath, dbFileName))
|
||||
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
)
|
||||
|
||||
var filesToRestore = filesToBackup
|
||||
var filesToRestore = append(filesToBackup, "portainer.db")
|
||||
|
||||
// Restores system state from backup archive, will trigger system shutdown, when finished.
|
||||
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/datastore/migrations"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
@@ -105,30 +108,51 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
if isNew {
|
||||
// from MigrateData
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
|
||||
err := updateSettingsFromFlags(store, flags)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed updating settings from flags: %v", err)
|
||||
}
|
||||
} else {
|
||||
storedVersion, err := store.VersionService.DBVersion()
|
||||
if err != nil {
|
||||
logrus.Fatalf("Something Failed during creation of new database: %v", err)
|
||||
}
|
||||
if storedVersion != portainer.DBVersion {
|
||||
err = store.MigrateData()
|
||||
m := migrations.NewMigrator(*store)
|
||||
err = m.Migrate(storedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed migration: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := migrations.NewMigrator(*store)
|
||||
|
||||
m.Migrate(18)
|
||||
|
||||
logrus.Fatal("Dieing.....")
|
||||
|
||||
err = updateSettingsFromFlags(store, flags)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed updating settings from flags: %v", err)
|
||||
log.Fatalf("Failed updating settings from flags: %v", err)
|
||||
}
|
||||
|
||||
// this is for the db restore functionality - needs more tests.
|
||||
go func() {
|
||||
<-shutdownCtx.Done()
|
||||
defer connection.Close()
|
||||
}()
|
||||
|
||||
exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
||||
|
||||
err := store.Export(exportFilename)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
|
||||
} else {
|
||||
logrus.Debugf("exported to %s", exportFilename)
|
||||
}
|
||||
connection.Close()
|
||||
}()
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -141,7 +165,14 @@ func initComposeStackManager(assetsPath string, configPath string, reverseTunnel
|
||||
return composeWrapper
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore) (portainer.SwarmStackManager, error) {
|
||||
func initSwarmStackManager(
|
||||
assetsPath string,
|
||||
configPath string,
|
||||
signatureService portainer.DigitalSignatureService,
|
||||
fileService portainer.FileService,
|
||||
reverseTunnelService portainer.ReverseTunnelService,
|
||||
dataStore dataservices.DataStore,
|
||||
) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||
}
|
||||
|
||||
@@ -375,7 +406,6 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
|
||||
TLSConfig: tlsConfiguration,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
@@ -437,7 +467,6 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
|
||||
@@ -33,4 +33,7 @@ type Connection interface {
|
||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
ConvertToKey(v int) []byte
|
||||
|
||||
BackupMetadata() (map[string]interface{}, error)
|
||||
RestoreMetadata(s map[string]interface{}) error
|
||||
}
|
||||
|
||||
@@ -376,3 +376,43 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
|
||||
buckets := map[string]interface{}{}
|
||||
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
bucket = tx.Bucket([]byte(bucketName))
|
||||
seqId := bucket.Sequence()
|
||||
buckets[bucketName] = int(seqId)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return buckets, err
|
||||
}
|
||||
|
||||
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
|
||||
var err error
|
||||
|
||||
for bucketName, v := range s {
|
||||
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
|
||||
if !ok {
|
||||
logrus.Errorf("Failed to restore metadata to bucket %s, skipped", bucketName)
|
||||
continue
|
||||
}
|
||||
|
||||
err = connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.SetSequence(uint64(id))
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package fdoprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ type (
|
||||
BackupTo(w io.Writer) error
|
||||
Export(filename string) (err error)
|
||||
IsErrObjectNotFound(err error) bool
|
||||
Connection() portainer.Connection
|
||||
|
||||
CustomTemplate() CustomTemplateService
|
||||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
|
||||
@@ -103,10 +103,6 @@ func (store *Store) IsErrObjectNotFound(e error) bool {
|
||||
return e == portainerErrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
func (store *Store) Connection() portainer.Connection {
|
||||
return store.connection
|
||||
}
|
||||
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
return store.connectionRollback(force)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, n
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
|
||||
53
api/datastore/migrations/1645580390_users_to_db_18.go
Normal file
53
api/datastore/migrations/1645580390_users_to_db_18.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 17,
|
||||
Timestamp: 1645580390,
|
||||
Up: v17_up_users_to_18,
|
||||
Down: v17_down_users_from_18,
|
||||
Name: "Users to 18",
|
||||
})
|
||||
}
|
||||
|
||||
func v17_up_users_to_18() error {
|
||||
legacyUsers, err := migrator.store.UserService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = map[portainer.Authorization]bool{
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
}
|
||||
|
||||
err = migrator.store.UserService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func v17_down_users_from_18() error {
|
||||
return nil
|
||||
}
|
||||
50
api/datastore/migrations/1645580396_endpoints_to_db_18.go
Normal file
50
api/datastore/migrations/1645580396_endpoints_to_db_18.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 17,
|
||||
Timestamp: 1645580396,
|
||||
Up: v17_up_endpoints_to_18,
|
||||
Down: v17_down_endpoints_from_18,
|
||||
Name: "Endpoints to 18",
|
||||
})
|
||||
}
|
||||
|
||||
func v17_up_endpoints_to_18() error {
|
||||
legacyEndpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range legacyEndpoints {
|
||||
endpoint.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||
for _, userID := range endpoint.AuthorizedUsers {
|
||||
endpoint.UserAccessPolicies[userID] = portainer.AccessPolicy{
|
||||
RoleID: 4,
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||
for _, teamID := range endpoint.AuthorizedTeams {
|
||||
endpoint.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
|
||||
RoleID: 4,
|
||||
}
|
||||
}
|
||||
|
||||
err = migrator.store.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v17_down_endpoints_from_18() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 17,
|
||||
Timestamp: 1645580400,
|
||||
Up: v17_up_endpoints_groups_to_18,
|
||||
Down: v17_down_endpoints_groups_from_18,
|
||||
Name: "Endpoints groups to 18",
|
||||
})
|
||||
}
|
||||
|
||||
func v17_up_endpoints_groups_to_18() error {
|
||||
legacyEndpointGroups, err := migrator.store.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range legacyEndpointGroups {
|
||||
endpointGroup.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||
for _, userID := range endpointGroup.AuthorizedUsers {
|
||||
endpointGroup.UserAccessPolicies[userID] = portainer.AccessPolicy{
|
||||
RoleID: 4,
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroup.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||
for _, teamID := range endpointGroup.AuthorizedTeams {
|
||||
endpointGroup.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
|
||||
RoleID: 4,
|
||||
}
|
||||
}
|
||||
|
||||
err = migrator.store.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v17_down_endpoints_groups_from_18() error {
|
||||
return nil
|
||||
}
|
||||
46
api/datastore/migrations/1645580420_registries_to_db_18.go
Normal file
46
api/datastore/migrations/1645580420_registries_to_db_18.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 17,
|
||||
Timestamp: 1645580420,
|
||||
Up: v17_up_registries_to_18,
|
||||
Down: v17_down_registries_to_18,
|
||||
Name: "Registries to 18",
|
||||
})
|
||||
}
|
||||
|
||||
func v17_up_registries_to_18() error {
|
||||
legacyRegistries, err := migrator.store.RegistryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range legacyRegistries {
|
||||
registry.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||
for _, userID := range registry.AuthorizedUsers {
|
||||
registry.UserAccessPolicies[userID] = portainer.AccessPolicy{}
|
||||
}
|
||||
|
||||
registry.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||
for _, teamID := range registry.AuthorizedTeams {
|
||||
registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{}
|
||||
}
|
||||
|
||||
err = migrator.store.RegistryService.UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v17_down_registries_to_18() error {
|
||||
return nil
|
||||
}
|
||||
33
api/datastore/migrations/1645677508_settings_to_db_19.go
Normal file
33
api/datastore/migrations/1645677508_settings_to_db_19.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 18,
|
||||
Timestamp: 1645677508,
|
||||
Up: v18_up_settings_to_db_19,
|
||||
Down: v18_down_settings_to_db_19,
|
||||
Name: "settings to db 19",
|
||||
})
|
||||
}
|
||||
|
||||
func v18_up_settings_to_db_19() error {
|
||||
legacySettings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.EdgeAgentCheckinInterval == 0 {
|
||||
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||
}
|
||||
|
||||
return migrator.store.SettingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func v18_down_settings_to_db_19() error {
|
||||
return nil
|
||||
}
|
||||
23
api/datastore/migrations/1645736810_users_to_db_20.go
Normal file
23
api/datastore/migrations/1645736810_users_to_db_20.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 19,
|
||||
Timestamp: 1645736810,
|
||||
Up: v19_up_users_to_db_20,
|
||||
Down: v19_down_users_to_db_20,
|
||||
Name: "users to db 20",
|
||||
})
|
||||
}
|
||||
|
||||
func v19_up_users_to_db_20() error {
|
||||
return migrator.store.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func v19_down_users_to_db_20() error {
|
||||
return nil
|
||||
}
|
||||
30
api/datastore/migrations/1645737700_settings_to_db_20.go
Normal file
30
api/datastore/migrations/1645737700_settings_to_db_20.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 19,
|
||||
Timestamp: 1645737700,
|
||||
Up: v19_up_settings_to_db_20,
|
||||
Down: v19_down_settings_to_db_20,
|
||||
Name: "settings to db 20",
|
||||
})
|
||||
}
|
||||
|
||||
func v19_up_settings_to_db_20() error {
|
||||
legacySettings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowVolumeBrowserForRegularUsers = false
|
||||
|
||||
return migrator.store.SettingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func v19_down_settings_to_db_20() error {
|
||||
return nil
|
||||
}
|
||||
58
api/datastore/migrations/1645737802_schedules_to_db_20.go
Normal file
58
api/datastore/migrations/1645737802_schedules_to_db_20.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
const scheduleScriptExecutionJobType = 1
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 19,
|
||||
Timestamp: 1645737802,
|
||||
Up: v19_up_schedules_to_db_20,
|
||||
Down: v19_down_schedules_to_db_20,
|
||||
Name: "schedules to db 20",
|
||||
})
|
||||
}
|
||||
|
||||
func v19_up_schedules_to_db_20() error {
|
||||
legacySchedules, err := migrator.store.ScheduleService.Schedules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, schedule := range legacySchedules {
|
||||
if schedule.JobType == scheduleScriptExecutionJobType {
|
||||
if schedule.CronExpression == "0 0 * * *" {
|
||||
schedule.CronExpression = "0 * * * *"
|
||||
} else if schedule.CronExpression == "0 0 0/2 * *" {
|
||||
schedule.CronExpression = "0 */2 * * *"
|
||||
} else if schedule.CronExpression == "0 0 0 * *" {
|
||||
schedule.CronExpression = "0 0 * * *"
|
||||
} else {
|
||||
revisedCronExpression := strings.Split(schedule.CronExpression, " ")
|
||||
if len(revisedCronExpression) == 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
revisedCronExpression = revisedCronExpression[1:]
|
||||
schedule.CronExpression = strings.Join(revisedCronExpression, " ")
|
||||
}
|
||||
|
||||
err := migrator.store.ScheduleService.UpdateSchedule(schedule.ID, &schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v19_down_schedules_to_db_20() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 20,
|
||||
Timestamp: 1646090591,
|
||||
Up: v20_up_resource_control_to_22,
|
||||
Down: v20_down_resource_control_to_22,
|
||||
Name: "resource control to 22",
|
||||
})
|
||||
}
|
||||
|
||||
func v20_up_resource_control_to_22() error {
|
||||
legacyResourceControls, err := migrator.store.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
|
||||
err := migrator.store.ResourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v20_down_resource_control_to_22() error {
|
||||
return nil
|
||||
}
|
||||
82
api/datastore/migrations/1646090646_user_and_roles_to_22.go
Normal file
82
api/datastore/migrations/1646090646_user_and_roles_to_22.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 20,
|
||||
Timestamp: 1646090646,
|
||||
Up: v20_up_user_and_roles_to_22,
|
||||
Down: v20_down_user_and_roles_to_22,
|
||||
Name: "user and roles to 22",
|
||||
})
|
||||
}
|
||||
|
||||
func v20_up_user_and_roles_to_22() error {
|
||||
legacyUsers, err := migrator.store.UserService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
|
||||
err = migrator.store.UserService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointAdministratorRole, err := migrator.store.RoleService.Role(portainer.RoleID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointAdministratorRole.Priority = 1
|
||||
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||
|
||||
err = migrator.store.RoleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
|
||||
|
||||
helpDeskRole, err := migrator.store.RoleService.Role(portainer.RoleID(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = migrator.store.RoleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
|
||||
|
||||
standardUserRole, err := migrator.store.RoleService.Role(portainer.RoleID(3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = migrator.store.RoleService.UpdateRole(standardUserRole.ID, standardUserRole)
|
||||
|
||||
readOnlyUserRole, err := migrator.store.RoleService.Role(portainer.RoleID(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = migrator.store.RoleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return migrator.store.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func v20_down_user_and_roles_to_22() error {
|
||||
return nil
|
||||
}
|
||||
39
api/datastore/migrations/1646090838_tags_to_23.go
Normal file
39
api/datastore/migrations/1646090838_tags_to_23.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 22,
|
||||
Timestamp: 1646090838,
|
||||
Up: v22_up_tags_to_23,
|
||||
Down: v22_down_tags_to_23,
|
||||
Name: "tags to 23",
|
||||
})
|
||||
}
|
||||
|
||||
func v22_up_tags_to_23() error {
|
||||
logrus.Info("Updating tags")
|
||||
tags, err := migrator.store.TagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
tag.EndpointGroups = make(map[portainer.EndpointGroupID]bool)
|
||||
tag.Endpoints = make(map[portainer.EndpointID]bool)
|
||||
err = migrator.store.TagService.UpdateTag(tag.ID, &tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func v22_down_tags_to_23() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 22,
|
||||
Timestamp: 1646091011,
|
||||
Up: v22_up_endpoints_and_endpoint_groups_to_23,
|
||||
Down: v22_down_endpoints_and_endpoint_groups_to_23,
|
||||
Name: "endpoints and endpoint groups to 23",
|
||||
})
|
||||
}
|
||||
|
||||
func v22_up_endpoints_and_endpoint_groups_to_23() error {
|
||||
logrus.Info("Updating endpoints and endpoint groups")
|
||||
tags, err := migrator.store.TagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagsNameMap := make(map[string]portainer.Tag)
|
||||
for _, tag := range tags {
|
||||
tagsNameMap[tag.Name] = tag
|
||||
}
|
||||
|
||||
endpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := make([]portainer.TagID, 0)
|
||||
for _, tagName := range endpoint.Tags {
|
||||
tag, ok := tagsNameMap[tagName]
|
||||
if ok {
|
||||
endpointTags = append(endpointTags, tag.ID)
|
||||
tag.Endpoints[endpoint.ID] = true
|
||||
}
|
||||
}
|
||||
endpoint.TagIDs = endpointTags
|
||||
err = migrator.store.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err = migrator.store.EndpointRelationService.Create(relation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroups, err := migrator.store.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
endpointGroupTags := make([]portainer.TagID, 0)
|
||||
for _, tagName := range endpointGroup.Tags {
|
||||
tag, ok := tagsNameMap[tagName]
|
||||
if ok {
|
||||
endpointGroupTags = append(endpointGroupTags, tag.ID)
|
||||
tag.EndpointGroups[endpointGroup.ID] = true
|
||||
}
|
||||
}
|
||||
endpointGroup.TagIDs = endpointGroupTags
|
||||
err = migrator.store.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, tag := range tagsNameMap {
|
||||
err = migrator.store.TagService.UpdateTag(tag.ID, &tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func v22_down_endpoints_and_endpoint_groups_to_23() error {
|
||||
return nil
|
||||
}
|
||||
41
api/datastore/migrations/1646091296_settings_to_24.go
Normal file
41
api/datastore/migrations/1646091296_settings_to_24.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 23,
|
||||
Timestamp: 1646091296,
|
||||
Up: v23_up_settings_to_24,
|
||||
Down: v23_down_settings_to_24,
|
||||
Name: "settings to 25",
|
||||
})
|
||||
}
|
||||
|
||||
func v23_up_settings_to_24() error {
|
||||
logrus.Info("Updating settings")
|
||||
|
||||
legacySettings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.TemplatesURL == "" {
|
||||
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
legacySettings.EnableTelemetry = true
|
||||
|
||||
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
|
||||
|
||||
return migrator.store.SettingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func v23_down_settings_to_24() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 24,
|
||||
Timestamp: 1646095944,
|
||||
Up: v24_up_endpoint_settings_to_25,
|
||||
Down: v24_down_endpoint_settings_to_25,
|
||||
Name: "endpoint settings to 25",
|
||||
})
|
||||
}
|
||||
|
||||
func v24_up_endpoint_settings_to_25() error {
|
||||
logrus.Info("Updating endpoint settings")
|
||||
settings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
endpoint := endpoints[i]
|
||||
|
||||
securitySettings := portainer.EndpointSecuritySettings{}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.DockerEnvironment {
|
||||
|
||||
securitySettings = portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
|
||||
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = migrator.store.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v24_down_endpoint_settings_to_25() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 26,
|
||||
Timestamp: 1646096711,
|
||||
Up: v26_up_stack_resource_control_to_27,
|
||||
Down: v26_down_stack_resource_control_to_27,
|
||||
Name: "stack resource control to 27",
|
||||
})
|
||||
}
|
||||
|
||||
func v26_up_stack_resource_control_to_27() error {
|
||||
logrus.Info("Updating stack resource controls")
|
||||
resourceControls, err := migrator.store.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resource := range resourceControls {
|
||||
if resource.Type != portainer.StackResourceControl {
|
||||
continue
|
||||
}
|
||||
|
||||
stackName := resource.ResourceID
|
||||
|
||||
stack, err := migrator.store.StackService.StackByName(stackName)
|
||||
if err != nil {
|
||||
if err == errors.ErrObjectNotFound {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
|
||||
|
||||
err = migrator.store.ResourceControlService.UpdateResourceControl(resource.ID, &resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v26_down_stack_resource_control_to_27() error {
|
||||
return nil
|
||||
}
|
||||
33
api/datastore/migrations/1646096869_settings_to_30.go
Normal file
33
api/datastore/migrations/1646096869_settings_to_30.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 29,
|
||||
Timestamp: 1646096869,
|
||||
Up: v29_up_settings_to_30,
|
||||
Down: v29_down_settings_to_30,
|
||||
Name: "settings to 30",
|
||||
})
|
||||
}
|
||||
|
||||
// so setting to false and "", is what would happen without this code
|
||||
// I'm going to bet there's zero point to changing the value inthe DB
|
||||
// Public for testing
|
||||
func v29_up_settings_to_30() error {
|
||||
legacySettings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.OAuthSettings.SSO = false
|
||||
legacySettings.OAuthSettings.LogoutURI = ""
|
||||
return migrator.store.SettingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func v29_down_settings_to_30() error {
|
||||
return nil
|
||||
}
|
||||
62
api/datastore/migrations/1646097709_registries_to_32.go
Normal file
62
api/datastore/migrations/1646097709_registries_to_32.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 31,
|
||||
Timestamp: 1646097709,
|
||||
Up: v31_up_registries_to_32,
|
||||
Down: v31_down_registries_to_32,
|
||||
Name: "registries to 32",
|
||||
})
|
||||
}
|
||||
|
||||
func v31_up_registries_to_32() error {
|
||||
registries, err := migrator.store.RegistryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
|
||||
registry.RegistryAccesses = portainer.RegistryAccesses{}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
filteredUserAccessPolicies := portainer.UserAccessPolicies{}
|
||||
for userId, registryPolicy := range registry.UserAccessPolicies {
|
||||
if _, found := endpoint.UserAccessPolicies[userId]; found {
|
||||
filteredUserAccessPolicies[userId] = registryPolicy
|
||||
}
|
||||
}
|
||||
|
||||
filteredTeamAccessPolicies := portainer.TeamAccessPolicies{}
|
||||
for teamId, registryPolicy := range registry.TeamAccessPolicies {
|
||||
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
|
||||
filteredTeamAccessPolicies[teamId] = registryPolicy
|
||||
}
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
|
||||
UserAccessPolicies: filteredUserAccessPolicies,
|
||||
TeamAccessPolicies: filteredTeamAccessPolicies,
|
||||
Namespaces: []string{},
|
||||
}
|
||||
}
|
||||
migrator.store.RegistryService.UpdateRegistry(registry.ID, ®istry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func v31_down_registries_to_32() error {
|
||||
return nil
|
||||
}
|
||||
109
api/datastore/migrations/1646097896_dockerhub_to_32.go
Normal file
109
api/datastore/migrations/1646097896_dockerhub_to_32.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 31,
|
||||
Timestamp: 1646097896,
|
||||
Up: v31_up_dockerhub_to_32,
|
||||
Down: v31_down_dockerhub_to_32,
|
||||
Name: "dockerhub to 32",
|
||||
})
|
||||
}
|
||||
|
||||
func v31_up_dockerhub_to_32() error {
|
||||
dockerhub, err := migrator.store.DockerHubService.DockerHub()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dockerhub.Authentication {
|
||||
return nil
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
Name: "Dockerhub (authenticated - migrated)",
|
||||
URL: "docker.io",
|
||||
Authentication: true,
|
||||
Username: dockerhub.Username,
|
||||
Password: dockerhub.Password,
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
// The following code will make this function idempotent.
|
||||
// i.e. if run again, it will not change the data. It will ensure that
|
||||
// we only have one migrated registry entry. Duplicates will be removed
|
||||
// if they exist and which has been happening due to earlier migration bugs
|
||||
migrated := false
|
||||
registries, _ := migrator.store.RegistryService.Registries()
|
||||
for _, r := range registries {
|
||||
if r.Type == registry.Type &&
|
||||
r.Name == registry.Name &&
|
||||
r.URL == registry.URL &&
|
||||
r.Authentication == registry.Authentication {
|
||||
|
||||
if !migrated {
|
||||
// keep this one entry
|
||||
migrated = true
|
||||
} else {
|
||||
// delete subsequent duplicates
|
||||
migrator.store.RegistryService.DeleteRegistry(portainer.RegistryID(r.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if migrated {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
if endpoint.Type != portainer.KubernetesLocalEnvironment &&
|
||||
endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
|
||||
endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
userAccessPolicies := portainer.UserAccessPolicies{}
|
||||
for userId := range endpoint.UserAccessPolicies {
|
||||
if _, found := endpoint.UserAccessPolicies[userId]; found {
|
||||
userAccessPolicies[userId] = portainer.AccessPolicy{
|
||||
RoleID: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teamAccessPolicies := portainer.TeamAccessPolicies{}
|
||||
for teamId := range endpoint.TeamAccessPolicies {
|
||||
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
|
||||
teamAccessPolicies[teamId] = portainer.AccessPolicy{
|
||||
RoleID: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
|
||||
UserAccessPolicies: userAccessPolicies,
|
||||
TeamAccessPolicies: teamAccessPolicies,
|
||||
Namespaces: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return migrator.store.RegistryService.Create(registry)
|
||||
}
|
||||
|
||||
func v31_down_dockerhub_to_32() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 31,
|
||||
Timestamp: 1646097916,
|
||||
Up: v31_up_volume_resource_control_to_32,
|
||||
Down: v31_down_volume_resource_control_to_32,
|
||||
Name: "volume resource control to 32",
|
||||
})
|
||||
}
|
||||
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
volumes := volumesData["Volumes"].([]interface{})
|
||||
for _, volumeMeta := range volumes {
|
||||
volume := volumeMeta.(map[string]interface{})
|
||||
volumeName, nameExist := volume["Name"].(string)
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
createTime, createTimeExist := volume["CreatedAt"].(string)
|
||||
if !createTimeExist {
|
||||
continue
|
||||
}
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
||||
if ok {
|
||||
toUpdate[resourceControl.ID] = fmt.Sprintf("%s_%s", volumeName, dockerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func v31_up_volume_resource_control_to_32() error {
|
||||
endpoints, err := migrator.store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environments: %w", err)
|
||||
}
|
||||
|
||||
resourceControls, err := migrator.store.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching resource controls: %w", err)
|
||||
}
|
||||
|
||||
toUpdate := map[portainer.ResourceControlID]string{}
|
||||
volumeResourceControls := map[string]*portainer.ResourceControl{}
|
||||
|
||||
for i := range resourceControls {
|
||||
resourceControl := resourceControls[i]
|
||||
if resourceControl.Type == portainer.VolumeResourceControl {
|
||||
volumeResourceControls[resourceControl.ResourceID] = &resourceControl
|
||||
}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if !endpointutils.IsDockerEndpoint(&endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
totalSnapshots := len(endpoint.Snapshots)
|
||||
if totalSnapshots == 0 {
|
||||
log.Println("[DEBUG] [volume migration] [message: no snapshot found]")
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot := endpoint.Snapshots[totalSnapshots-1]
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [database,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
|
||||
if volumesData["Volumes"] == nil {
|
||||
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
|
||||
continue
|
||||
}
|
||||
|
||||
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
|
||||
}
|
||||
}
|
||||
|
||||
for _, resourceControl := range volumeResourceControls {
|
||||
if newResourceID, ok := toUpdate[resourceControl.ID]; ok {
|
||||
resourceControl.ResourceID = newResourceID
|
||||
err := migrator.store.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed updating resource control %d: %w", resourceControl.ID, err)
|
||||
}
|
||||
|
||||
} else {
|
||||
err := migrator.store.ResourceControlService.DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
|
||||
}
|
||||
log.Printf("[DEBUG] [volume migration] [message: legacy resource control(%s) has been deleted]", resourceControl.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func v31_down_volume_resource_control_to_32() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 31,
|
||||
Timestamp: 1646097944,
|
||||
Up: v31_up_kubeconfig_expiry_to_32,
|
||||
Down: v31_down_kubeconfig_expiry_to_32,
|
||||
Name: "kubeconfig expiry to 32",
|
||||
})
|
||||
}
|
||||
|
||||
func v31_up_kubeconfig_expiry_to_32() error {
|
||||
settings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
|
||||
return migrator.store.SettingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func v31_down_kubeconfig_expiry_to_32() error {
|
||||
return nil
|
||||
}
|
||||
29
api/datastore/migrations/1646097962_helm_repo_url_to_32.go
Normal file
29
api/datastore/migrations/1646097962_helm_repo_url_to_32.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: 31,
|
||||
Timestamp: 1646097962,
|
||||
Up: v31_up_helm_repo_url_to_32,
|
||||
Down: v31_down_helm_repo_url_to_32,
|
||||
Name: "helm repo url to 32",
|
||||
})
|
||||
}
|
||||
|
||||
func v31_up_helm_repo_url_to_32() error {
|
||||
settings, err := migrator.store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HelmRepositoryURL = portainer.DefaultHelmRepositoryURL
|
||||
return migrator.store.SettingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func v31_down_helm_repo_url_to_32() error {
|
||||
return nil
|
||||
}
|
||||
39
api/datastore/migrations/migration.sh
Executable file
39
api/datastore/migrations/migration.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
die () {
|
||||
echo >&2 "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || die "Usage - version \"space separated context\""
|
||||
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION=$1
|
||||
CONTEXT=$2
|
||||
|
||||
CONTEXT_SLUG="${CONTEXT// /_}"
|
||||
|
||||
cat << EOF >${TIMESTAMP}_${CONTEXT_SLUG}.go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrator.AddMigration(types.Migration{
|
||||
Version: ${VERSION},
|
||||
Timestamp: ${TIMESTAMP},
|
||||
Up: v${VERSION}_up_${CONTEXT_SLUG},
|
||||
Down: v${VERSION}_down_${CONTEXT_SLUG},
|
||||
Name: "${CONTEXT}",
|
||||
})
|
||||
}
|
||||
|
||||
func v${VERSION}_up_${CONTEXT_SLUG}() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func v${VERSION}_down_${CONTEXT_SLUG}() error {
|
||||
return nil
|
||||
}
|
||||
EOF
|
||||
111
api/datastore/migrations/migrator.go
Normal file
111
api/datastore/migrations/migrator.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/datastore/migrations/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type apiDBVersion struct {
|
||||
api string
|
||||
db int
|
||||
}
|
||||
type apiVersionsMap []apiDBVersion
|
||||
|
||||
type migrations []*types.Migration
|
||||
|
||||
type Migrator struct {
|
||||
store datastore.Store
|
||||
Versions []int
|
||||
Migrations map[int]migrations
|
||||
}
|
||||
|
||||
var migrator *Migrator = &Migrator{
|
||||
Versions: []int{},
|
||||
Migrations: map[int]migrations{},
|
||||
}
|
||||
|
||||
var versionsMap apiVersionsMap = apiVersionsMap{
|
||||
{"2.12.0", 36},
|
||||
{"2.9.3", 35},
|
||||
{"2.10.0", 34},
|
||||
{"2.9.2", 33},
|
||||
{"2.9.1", 33},
|
||||
{"2.9.0", 32},
|
||||
{"2.7.0", 31},
|
||||
{"2.6.0", 30},
|
||||
{"2.4.0", 29},
|
||||
{"2.4.0", 28},
|
||||
{"2.2.0", 27},
|
||||
{"2.1.0", 26},
|
||||
}
|
||||
|
||||
func NewMigrator(m datastore.Store) *Migrator {
|
||||
migrator.store = m
|
||||
return migrator
|
||||
}
|
||||
|
||||
func (m *Migrator) AddMigration(mg types.Migration) {
|
||||
// Add the migration to the hash with version as key
|
||||
if m.Migrations[mg.Version] == nil {
|
||||
m.Migrations[mg.Version] = make(migrations, 0)
|
||||
}
|
||||
m.Migrations[mg.Version] = append(m.Migrations[mg.Version], &mg)
|
||||
|
||||
if !contains(m.Versions, mg.Version) {
|
||||
// Insert version into versions array using insertion sort
|
||||
index := 0
|
||||
for index < len(m.Versions) {
|
||||
if m.Versions[index] > mg.Version {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
m.Versions = append(m.Versions, mg.Version)
|
||||
copy(m.Versions[index+1:], m.Versions[index:])
|
||||
m.Versions[index] = mg.Version
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) Migrate(currentVersion int) error {
|
||||
migrationsToRun := &Migrator{
|
||||
Versions: []int{},
|
||||
Migrations: map[int]migrations{},
|
||||
}
|
||||
for _, v := range m.Versions {
|
||||
mg := m.Migrations[v]
|
||||
// if migration version is below current version
|
||||
if v < currentVersion {
|
||||
continue
|
||||
}
|
||||
migrationsToRun.Versions = append(migrationsToRun.Versions, v)
|
||||
migrationsToRun.Migrations[v] = mg
|
||||
}
|
||||
|
||||
// TODO: Sort by Timestamp
|
||||
for _, v := range migrationsToRun.Versions {
|
||||
mg := m.Migrations[v]
|
||||
for _, m := range mg {
|
||||
logger := logrus.WithFields(logrus.Fields{"version": m.Version, "migration": m.Name})
|
||||
logger.Info("starting migration")
|
||||
err := m.Up()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("while running migration for version %d, name `%s`", m.Version, m.Name))
|
||||
}
|
||||
m.Completed = true
|
||||
logger.Info("migration completed successfully")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move to utils
|
||||
func contains(s []int, searchterm int) bool {
|
||||
i := sort.SearchInts(s, searchterm)
|
||||
return i < len(s) && s[i] == searchterm
|
||||
}
|
||||
10
api/datastore/migrations/types/types.go
Normal file
10
api/datastore/migrations/types/types.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package types
|
||||
|
||||
type Migration struct {
|
||||
Version int
|
||||
Up func() error
|
||||
Down func() error
|
||||
Completed bool
|
||||
Timestamp int32
|
||||
Name string
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/user"
|
||||
"github.com/portainer/portainer/api/dataservices/version"
|
||||
"github.com/portainer/portainer/api/dataservices/webhook"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -68,6 +69,8 @@ type Store struct {
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
|
||||
AuthorizationService *authorization.Service // TODO: validate why it is not part of store
|
||||
}
|
||||
|
||||
func (store *Store) initServices() error {
|
||||
@@ -227,6 +230,13 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ScheduleService = scheduleService
|
||||
|
||||
authService := authorization.NewService(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.AuthorizationService = authService
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -369,6 +379,7 @@ type storeExport struct {
|
||||
User []portainer.User `json:"users,omitempty"`
|
||||
Version map[string]string `json:"version,omitempty"`
|
||||
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (store *Store) Export(filename string) (err error) {
|
||||
@@ -561,6 +572,11 @@ func (store *Store) Export(filename string) (err error) {
|
||||
"INSTANCE_ID": instance,
|
||||
}
|
||||
|
||||
backup.Metadata, err = store.connection.BackupMetadata()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Errorf("Exporting Metadata")
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(backup, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -569,6 +585,7 @@ func (store *Store) Export(filename string) (err error) {
|
||||
}
|
||||
|
||||
func (store *Store) Import(filename string) (err error) {
|
||||
|
||||
backup := storeExport{}
|
||||
|
||||
s, err := ioutil.ReadFile(filename)
|
||||
@@ -669,5 +686,5 @@ func (store *Store) Import(filename string) (err error) {
|
||||
store.Webhook().UpdateWebhook(v.ID, &v)
|
||||
}
|
||||
|
||||
return nil
|
||||
return store.connection.RestoreMetadata(backup.Metadata)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ require (
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
|
||||
@@ -619,8 +619,8 @@ github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc h1:vxVN9srGND+iA9oBmyFgtbtOvnmOCLmxw20ncYCJ5HA=
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc/go.mod h1:nyQA6IahOruIvENCcBk54aaUvV2WHFdXkvBjIutg+SY=
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f h1:GMIjRVV2LADpJprPG2+8MdRH6XvrFgC7wHm7dFUdOpc=
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f/go.mod h1:nyQA6IahOruIvENCcBk54aaUvV2WHFdXkvBjIutg+SY=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -34,7 +34,5 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/agent/kubernetes").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI)))
|
||||
h.PathPrefix("/{id}/storidge").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package endpointproxy
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
var storidgeExtension *portainer.EndpointExtension
|
||||
for _, extension := range endpoint.Extensions {
|
||||
if extension.Type == portainer.StoridgeEndpointExtension {
|
||||
storidgeExtension = &extension
|
||||
}
|
||||
}
|
||||
|
||||
if storidgeExtension == nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this environment", errors.New("This extension is not supported")}
|
||||
}
|
||||
|
||||
proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetLegacyExtensionProxy(proxyExtensionKey)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateLegacyExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy", err}
|
||||
}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
http.StripPrefix("/"+id+"/storidge", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
@@ -284,7 +284,6 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
PublicURL: payload.PublicURL,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
AzureCredentials: credentials,
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
@@ -330,7 +329,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
@@ -385,7 +383,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
@@ -421,7 +418,6 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
@@ -451,7 +447,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type endpointExtensionAddPayload struct {
|
||||
Type int
|
||||
URL string
|
||||
}
|
||||
|
||||
func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
|
||||
if payload.Type != 1 {
|
||||
return errors.New("Invalid type value. Value must be one of: 1 (Storidge)")
|
||||
}
|
||||
if payload.Type == 1 && govalidator.IsNull(payload.URL) {
|
||||
return errors.New("Invalid extension URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id endpointExtensionAdd
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions [post]
|
||||
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload endpointExtensionAddPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionType := portainer.EndpointExtensionType(payload.Type)
|
||||
|
||||
var extension *portainer.EndpointExtension
|
||||
for idx := range endpoint.Extensions {
|
||||
if endpoint.Extensions[idx].Type == extensionType {
|
||||
extension = &endpoint.Extensions[idx]
|
||||
}
|
||||
}
|
||||
|
||||
if extension != nil {
|
||||
extension.URL = payload.URL
|
||||
} else {
|
||||
extension = &portainer.EndpointExtension{
|
||||
Type: extensionType,
|
||||
URL: payload.URL,
|
||||
}
|
||||
endpoint.Extensions = append(endpoint.Extensions, *extension)
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, extension)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// @id endpointExtensionRemove
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param extensionType path string true "Extension Type"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions/{extensionType} [delete]
|
||||
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension type route variable", err}
|
||||
}
|
||||
|
||||
for idx, ext := range endpoint.Extensions {
|
||||
if ext.Type == portainer.EndpointExtensionType(extensionType) {
|
||||
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -62,10 +62,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/dockerhub/{registryId}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/extensions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/status",
|
||||
|
||||
@@ -80,7 +80,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.11.1
|
||||
// @version 2.11.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -190,8 +190,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/kubernetes/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/storidge/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/azure/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/agent/"):
|
||||
|
||||
@@ -2,9 +2,8 @@ package registries
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
@@ -116,18 +116,22 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
if *payload.HelmRepositoryURL != "" {
|
||||
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
|
||||
}
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
|
||||
}
|
||||
}
|
||||
|
||||
settings.HelmRepositoryURL = newHelmRepo
|
||||
} else {
|
||||
settings.HelmRepositoryURL = ""
|
||||
settings.HelmRepositoryURL = newHelmRepo
|
||||
} else {
|
||||
settings.HelmRepositoryURL = ""
|
||||
}
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
|
||||
@@ -211,9 +211,6 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
|
||||
}
|
||||
|
||||
case portainer.KubernetesStack:
|
||||
if stack.Namespace == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
|
||||
}
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
|
||||
|
||||
@@ -3,6 +3,7 @@ package users
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -99,6 +100,7 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure}
|
||||
}
|
||||
user.TokenIssueAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
if payload.Role != 0 {
|
||||
@@ -116,6 +118,5 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
|
||||
// remove all of the users persisted API keys
|
||||
handler.apiKeyService.InvalidateUserKeyCache(user.ID)
|
||||
|
||||
return response.JSON(w, user)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package users
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -85,6 +86,8 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure}
|
||||
}
|
||||
|
||||
user.TokenIssueAt = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.User().UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
// Handler is the HTTP handler used to handle webhook operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore dataservices.DataStore
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
@@ -20,7 +23,8 @@ type Handler struct {
|
||||
// NewHandler creates a handler to manage webhooks operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
h.Handle("/webhooks",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
||||
@@ -34,3 +38,43 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkResourceAccess(r *http.Request, resourceID string, resourceControlType portainer.ResourceControlType) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
// non-admins
|
||||
rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceID, resourceControlType)
|
||||
if rc == nil || err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the resource", Err: err}
|
||||
}
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range securityContext.UserMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
canAccess := authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, rc)
|
||||
if !canAccess {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "This operation is disabled for non-admin users and unassigned access users"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) checkAuthorization(r *http.Request, endpoint *portainer.Endpoint, authorizations []portainer.Authorization) (bool, *httperror.HandlerError) {
|
||||
err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
authService := authorization.NewService(handler.DataStore)
|
||||
isAdminOrAuthorized, err := authService.UserIsAdminOrAuthorized(securityContext.UserID, endpoint.ID, authorizations)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to get user authorizations", Err: err}
|
||||
}
|
||||
return isAdminOrAuthorized, nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ package webhooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/registryutils/access"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gofrs/uuid"
|
||||
@@ -65,6 +64,15 @@ func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
endpointID := portainer.EndpointID(payload.EndpointID)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to create a webhook", Err: errors.New("not authorized to create a webhook")}
|
||||
}
|
||||
|
||||
if payload.RegistryID != 0 {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -25,6 +27,15 @@ func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid webhook id", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to delete a webhook", Err: errors.New("not authorized to delete a webhook")}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Webhook().DeleteWebhook(portainer.WebhookID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the webhook from the database", err}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -111,7 +112,15 @@ func (handler *Handler) executeServiceWebhook(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if imageTag != "" {
|
||||
rc, err := dockerClient.ImagePull(context.Background(), service.Spec.TaskTemplate.ContainerSpec.Image, dockertypes.ImagePullOptions{RegistryAuth: serviceUpdateOptions.EncodedRegistryAuth})
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Error pulling image with the specified tag", Err: err}
|
||||
}
|
||||
defer func(rc io.ReadCloser) {
|
||||
_ = rc.Close()
|
||||
}(rc)
|
||||
}
|
||||
_, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, serviceUpdateOptions)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -33,6 +34,14 @@ func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return response.JSON(w, []portainer.Webhook{})
|
||||
}
|
||||
|
||||
webhooks, err := handler.DataStore.Webhook().Webhooks()
|
||||
webhooks = filterWebhooks(webhooks, &filters)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -53,6 +54,15 @@ func (handler *Handler) webhookUpdate(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a webhooks with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to update a webhook", Err: errors.New("not authorized to update a webhook")}
|
||||
}
|
||||
|
||||
if payload.RegistryID != 0 {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,8 +2,6 @@ package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
@@ -40,18 +38,6 @@ func NewProxyFactory(dataStore dataservices.DataStore, signatureService portaine
|
||||
}
|
||||
}
|
||||
|
||||
// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge)
|
||||
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensionURL.Scheme = "http"
|
||||
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an environment(endpoint) API server
|
||||
func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
switch endpoint.Type {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (transport *baseTransport) refreshRegistry(request *http.Request, namespace string) (err error) {
|
||||
|
||||
@@ -15,25 +15,21 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
)
|
||||
|
||||
// TODO: contain code related to legacy extension management
|
||||
|
||||
type (
|
||||
// Manager represents a service used to manage proxies to environments(endpoints) and extensions.
|
||||
// Manager represents a service used to manage proxies to environments (endpoints).
|
||||
Manager struct {
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
k8sClientFactory *cli.ClientFactory
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
k8sClientFactory *cli.ClientFactory
|
||||
}
|
||||
)
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager {
|
||||
return &Manager{
|
||||
endpointProxies: cmap.New(),
|
||||
legacyExtensionProxies: cmap.New(),
|
||||
k8sClientFactory: kubernetesClientFactory,
|
||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||
endpointProxies: cmap.New(),
|
||||
k8sClientFactory: kubernetesClientFactory,
|
||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,26 +69,6 @@ func (manager *Manager) DeleteEndpointProxy(endpointID portainer.EndpointID) {
|
||||
manager.k8sClientFactory.RemoveKubeClient(endpointID)
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.legacyExtensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
||||
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API
|
||||
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
|
||||
return manager.proxyFactory.NewGitlabProxy(url)
|
||||
|
||||
@@ -153,7 +153,6 @@ func DefaultEndpointAuthorizationsForEndpointAdministratorRole() portainer.Autho
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.OperationPortainerWebhookCreate: true,
|
||||
portainer.OperationPortainerWebhookDelete: true,
|
||||
portainer.OperationIntegrationStoridgeAdmin: true,
|
||||
portainer.EndpointResourcesAccess: true,
|
||||
}
|
||||
}
|
||||
@@ -412,21 +411,19 @@ func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizatio
|
||||
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
||||
func DefaultPortainerAuthorizations() portainer.Authorizations {
|
||||
return map[portainer.Authorization]bool{
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserInspect: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserInspect: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,3 +600,21 @@ func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []port
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
func (service *Service) UserIsAdminOrAuthorized(userID portainer.UserID, endpointID portainer.EndpointID, authorizations []portainer.Authorization) (bool, error) {
|
||||
user, err := service.dataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, authorization := range authorizations {
|
||||
_, authorized := user.EndpointAuthorizations[endpointID][authorization]
|
||||
if authorized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"io"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
@@ -33,7 +32,6 @@ type testDatastore struct {
|
||||
user dataservices.UserService
|
||||
version dataservices.VersionService
|
||||
webhook dataservices.WebhookService
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (d *testDatastore) BackupTo(io.Writer) error { return nil }
|
||||
@@ -81,10 +79,6 @@ func (d *testDatastore) IsErrObjectNotFound(e error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *testDatastore) Connection() portainer.Connection {
|
||||
return d.connection
|
||||
}
|
||||
|
||||
func (d *testDatastore) Export(filename string) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -97,12 +91,10 @@ type datastoreOption = func(d *testDatastore)
|
||||
// NewDatastore creates new instance of testDatastore.
|
||||
// Will apply options before returning, opts will be applied from left to right.
|
||||
func NewDatastore(options ...datastoreOption) *testDatastore {
|
||||
conn, _ := database.NewDatabase("boltdb", "", nil)
|
||||
d := testDatastore{connection: conn}
|
||||
d := testDatastore{}
|
||||
for _, o := range options {
|
||||
o(&d)
|
||||
}
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,14 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
|
||||
|
||||
if err == nil && parsedToken != nil {
|
||||
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
|
||||
|
||||
user, err := service.dataStore.User().User(portainer.UserID(cl.UserID))
|
||||
if err != nil {
|
||||
return nil, errInvalidJWTToken
|
||||
}
|
||||
if user.TokenIssueAt > cl.StandardClaims.IssuedAt {
|
||||
return nil, errInvalidJWTToken
|
||||
}
|
||||
return &portainer.TokenData{
|
||||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
@@ -162,6 +170,7 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt
|
||||
Scope: scope,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
IssuedAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
208
api/portainer.go
208
api/portainer.go
@@ -296,10 +296,9 @@ type (
|
||||
// Environment(Endpoint) group identifier
|
||||
GroupID EndpointGroupID `json:"GroupId" example:"1"`
|
||||
// URL or IP address where exposed containers will be reachable
|
||||
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
Extensions []EndpointExtension `json:"Extensions" example:""`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
|
||||
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
|
||||
// List of tag identifiers to which this environment(endpoint) is associated
|
||||
TagIDs []TagID `json:"TagIds"`
|
||||
// The status of the environment(endpoint) (1 - up, 2 - down)
|
||||
@@ -349,17 +348,6 @@ type (
|
||||
// EndpointAuthorizations represents the authorizations associated to a set of environments(endpoints)
|
||||
EndpointAuthorizations map[EndpointID]Authorizations
|
||||
|
||||
// EndpointExtension represents a deprecated form of Portainer extension
|
||||
// TODO: legacy extension management
|
||||
EndpointExtension struct {
|
||||
Type EndpointExtensionType `json:"Type"`
|
||||
URL string `json:"URL"`
|
||||
}
|
||||
|
||||
// EndpointExtensionType represents the type of an environment(endpoint) extension. Only
|
||||
// one extension of each type can be associated to an environment(endpoint)
|
||||
EndpointExtensionType int
|
||||
|
||||
// EndpointGroup represents a group of environments(endpoints)
|
||||
EndpointGroup struct {
|
||||
// Environment(Endpoint) group Identifier
|
||||
@@ -1138,7 +1126,8 @@ type (
|
||||
// User Theme
|
||||
UserTheme string `example:"dark"`
|
||||
// User role (1 for administrator account and 2 for regular account)
|
||||
Role UserRole `json:"Role" example:"1"`
|
||||
Role UserRole `json:"Role" example:"1"`
|
||||
TokenIssueAt int64 `json:"TokenIssueAt" example:"1"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 25
|
||||
@@ -1347,7 +1336,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.11.1"
|
||||
APIVersion = "2.11.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 35
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1449,12 +1438,6 @@ const (
|
||||
StatusAcknowledged
|
||||
)
|
||||
|
||||
const (
|
||||
_ EndpointExtensionType = iota
|
||||
// StoridgeEndpointExtension represents the Storidge extension
|
||||
StoridgeEndpointExtension
|
||||
)
|
||||
|
||||
const (
|
||||
_ EndpointStatus = iota
|
||||
// EndpointStatusUp is used to represent an available environment(endpoint)
|
||||
@@ -1729,101 +1712,102 @@ const (
|
||||
OperationDockerAgentBrowsePut Authorization = "DockerAgentBrowsePut"
|
||||
OperationDockerAgentBrowseRename Authorization = "DockerAgentBrowseRename"
|
||||
|
||||
OperationPortainerDockerHubInspect Authorization = "PortainerDockerHubInspect"
|
||||
OperationPortainerDockerHubUpdate Authorization = "PortainerDockerHubUpdate"
|
||||
OperationPortainerEndpointGroupCreate Authorization = "PortainerEndpointGroupCreate"
|
||||
OperationPortainerEndpointGroupList Authorization = "PortainerEndpointGroupList"
|
||||
OperationPortainerEndpointGroupDelete Authorization = "PortainerEndpointGroupDelete"
|
||||
OperationPortainerEndpointGroupInspect Authorization = "PortainerEndpointGroupInspect"
|
||||
OperationPortainerEndpointGroupUpdate Authorization = "PortainerEndpointGroupEdit"
|
||||
OperationPortainerEndpointGroupAccess Authorization = "PortainerEndpointGroupAccess "
|
||||
OperationPortainerEndpointList Authorization = "PortainerEndpointList"
|
||||
OperationPortainerEndpointInspect Authorization = "PortainerEndpointInspect"
|
||||
OperationPortainerEndpointCreate Authorization = "PortainerEndpointCreate"
|
||||
OperationPortainerEndpointExtensionAdd Authorization = "PortainerEndpointExtensionAdd"
|
||||
OperationPortainerEndpointJob Authorization = "PortainerEndpointJob"
|
||||
OperationPortainerEndpointSnapshots Authorization = "PortainerEndpointSnapshots"
|
||||
OperationPortainerEndpointSnapshot Authorization = "PortainerEndpointSnapshot"
|
||||
OperationPortainerEndpointUpdate Authorization = "PortainerEndpointUpdate"
|
||||
OperationPortainerEndpointUpdateAccess Authorization = "PortainerEndpointUpdateAccess"
|
||||
OperationPortainerEndpointDelete Authorization = "PortainerEndpointDelete"
|
||||
OperationPortainerEndpointExtensionRemove Authorization = "PortainerEndpointExtensionRemove"
|
||||
OperationPortainerExtensionList Authorization = "PortainerExtensionList"
|
||||
OperationPortainerExtensionInspect Authorization = "PortainerExtensionInspect"
|
||||
OperationPortainerExtensionCreate Authorization = "PortainerExtensionCreate"
|
||||
OperationPortainerExtensionUpdate Authorization = "PortainerExtensionUpdate"
|
||||
OperationPortainerExtensionDelete Authorization = "PortainerExtensionDelete"
|
||||
OperationPortainerMOTD Authorization = "PortainerMOTD"
|
||||
OperationPortainerRegistryList Authorization = "PortainerRegistryList"
|
||||
OperationPortainerRegistryInspect Authorization = "PortainerRegistryInspect"
|
||||
OperationPortainerRegistryCreate Authorization = "PortainerRegistryCreate"
|
||||
OperationPortainerRegistryConfigure Authorization = "PortainerRegistryConfigure"
|
||||
OperationPortainerRegistryUpdate Authorization = "PortainerRegistryUpdate"
|
||||
OperationPortainerRegistryUpdateAccess Authorization = "PortainerRegistryUpdateAccess"
|
||||
OperationPortainerRegistryDelete Authorization = "PortainerRegistryDelete"
|
||||
OperationPortainerResourceControlCreate Authorization = "PortainerResourceControlCreate"
|
||||
OperationPortainerResourceControlUpdate Authorization = "PortainerResourceControlUpdate"
|
||||
OperationPortainerResourceControlDelete Authorization = "PortainerResourceControlDelete"
|
||||
OperationPortainerRoleList Authorization = "PortainerRoleList"
|
||||
OperationPortainerRoleInspect Authorization = "PortainerRoleInspect"
|
||||
OperationPortainerRoleCreate Authorization = "PortainerRoleCreate"
|
||||
OperationPortainerRoleUpdate Authorization = "PortainerRoleUpdate"
|
||||
OperationPortainerRoleDelete Authorization = "PortainerRoleDelete"
|
||||
OperationPortainerScheduleList Authorization = "PortainerScheduleList"
|
||||
OperationPortainerScheduleInspect Authorization = "PortainerScheduleInspect"
|
||||
OperationPortainerScheduleFile Authorization = "PortainerScheduleFile"
|
||||
OperationPortainerScheduleTasks Authorization = "PortainerScheduleTasks"
|
||||
OperationPortainerScheduleCreate Authorization = "PortainerScheduleCreate"
|
||||
OperationPortainerScheduleUpdate Authorization = "PortainerScheduleUpdate"
|
||||
OperationPortainerScheduleDelete Authorization = "PortainerScheduleDelete"
|
||||
OperationPortainerSettingsInspect Authorization = "PortainerSettingsInspect"
|
||||
OperationPortainerSettingsUpdate Authorization = "PortainerSettingsUpdate"
|
||||
OperationPortainerSettingsLDAPCheck Authorization = "PortainerSettingsLDAPCheck"
|
||||
OperationPortainerStackList Authorization = "PortainerStackList"
|
||||
OperationPortainerStackInspect Authorization = "PortainerStackInspect"
|
||||
OperationPortainerStackFile Authorization = "PortainerStackFile"
|
||||
OperationPortainerStackCreate Authorization = "PortainerStackCreate"
|
||||
OperationPortainerStackMigrate Authorization = "PortainerStackMigrate"
|
||||
OperationPortainerStackUpdate Authorization = "PortainerStackUpdate"
|
||||
OperationPortainerStackDelete Authorization = "PortainerStackDelete"
|
||||
OperationPortainerTagList Authorization = "PortainerTagList"
|
||||
OperationPortainerTagCreate Authorization = "PortainerTagCreate"
|
||||
OperationPortainerTagDelete Authorization = "PortainerTagDelete"
|
||||
OperationPortainerTeamMembershipList Authorization = "PortainerTeamMembershipList"
|
||||
OperationPortainerTeamMembershipCreate Authorization = "PortainerTeamMembershipCreate"
|
||||
OperationPortainerTeamMembershipUpdate Authorization = "PortainerTeamMembershipUpdate"
|
||||
OperationPortainerTeamMembershipDelete Authorization = "PortainerTeamMembershipDelete"
|
||||
OperationPortainerTeamList Authorization = "PortainerTeamList"
|
||||
OperationPortainerTeamInspect Authorization = "PortainerTeamInspect"
|
||||
OperationPortainerTeamMemberships Authorization = "PortainerTeamMemberships"
|
||||
OperationPortainerTeamCreate Authorization = "PortainerTeamCreate"
|
||||
OperationPortainerTeamUpdate Authorization = "PortainerTeamUpdate"
|
||||
OperationPortainerTeamDelete Authorization = "PortainerTeamDelete"
|
||||
OperationPortainerTemplateList Authorization = "PortainerTemplateList"
|
||||
OperationPortainerTemplateInspect Authorization = "PortainerTemplateInspect"
|
||||
OperationPortainerTemplateCreate Authorization = "PortainerTemplateCreate"
|
||||
OperationPortainerTemplateUpdate Authorization = "PortainerTemplateUpdate"
|
||||
OperationPortainerTemplateDelete Authorization = "PortainerTemplateDelete"
|
||||
OperationPortainerUploadTLS Authorization = "PortainerUploadTLS"
|
||||
OperationPortainerUserList Authorization = "PortainerUserList"
|
||||
OperationPortainerUserInspect Authorization = "PortainerUserInspect"
|
||||
OperationPortainerUserMemberships Authorization = "PortainerUserMemberships"
|
||||
OperationPortainerUserCreate Authorization = "PortainerUserCreate"
|
||||
OperationPortainerUserUpdate Authorization = "PortainerUserUpdate"
|
||||
OperationPortainerUserUpdatePassword Authorization = "PortainerUserUpdatePassword"
|
||||
OperationPortainerUserDelete Authorization = "PortainerUserDelete"
|
||||
OperationPortainerWebsocketExec Authorization = "PortainerWebsocketExec"
|
||||
OperationPortainerWebhookList Authorization = "PortainerWebhookList"
|
||||
OperationPortainerWebhookCreate Authorization = "PortainerWebhookCreate"
|
||||
OperationPortainerWebhookDelete Authorization = "PortainerWebhookDelete"
|
||||
|
||||
OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin"
|
||||
OperationPortainerDockerHubInspect Authorization = "PortainerDockerHubInspect"
|
||||
OperationPortainerDockerHubUpdate Authorization = "PortainerDockerHubUpdate"
|
||||
OperationPortainerEndpointGroupCreate Authorization = "PortainerEndpointGroupCreate"
|
||||
OperationPortainerEndpointGroupList Authorization = "PortainerEndpointGroupList"
|
||||
OperationPortainerEndpointGroupDelete Authorization = "PortainerEndpointGroupDelete"
|
||||
OperationPortainerEndpointGroupInspect Authorization = "PortainerEndpointGroupInspect"
|
||||
OperationPortainerEndpointGroupUpdate Authorization = "PortainerEndpointGroupEdit"
|
||||
OperationPortainerEndpointGroupAccess Authorization = "PortainerEndpointGroupAccess "
|
||||
OperationPortainerEndpointList Authorization = "PortainerEndpointList"
|
||||
OperationPortainerEndpointInspect Authorization = "PortainerEndpointInspect"
|
||||
OperationPortainerEndpointCreate Authorization = "PortainerEndpointCreate"
|
||||
OperationPortainerEndpointJob Authorization = "PortainerEndpointJob"
|
||||
OperationPortainerEndpointSnapshots Authorization = "PortainerEndpointSnapshots"
|
||||
OperationPortainerEndpointSnapshot Authorization = "PortainerEndpointSnapshot"
|
||||
OperationPortainerEndpointUpdate Authorization = "PortainerEndpointUpdate"
|
||||
OperationPortainerEndpointUpdateAccess Authorization = "PortainerEndpointUpdateAccess"
|
||||
OperationPortainerEndpointDelete Authorization = "PortainerEndpointDelete"
|
||||
OperationPortainerExtensionList Authorization = "PortainerExtensionList"
|
||||
OperationPortainerExtensionInspect Authorization = "PortainerExtensionInspect"
|
||||
OperationPortainerExtensionCreate Authorization = "PortainerExtensionCreate"
|
||||
OperationPortainerExtensionUpdate Authorization = "PortainerExtensionUpdate"
|
||||
OperationPortainerExtensionDelete Authorization = "PortainerExtensionDelete"
|
||||
OperationPortainerMOTD Authorization = "PortainerMOTD"
|
||||
OperationPortainerRegistryList Authorization = "PortainerRegistryList"
|
||||
OperationPortainerRegistryInspect Authorization = "PortainerRegistryInspect"
|
||||
OperationPortainerRegistryCreate Authorization = "PortainerRegistryCreate"
|
||||
OperationPortainerRegistryConfigure Authorization = "PortainerRegistryConfigure"
|
||||
OperationPortainerRegistryUpdate Authorization = "PortainerRegistryUpdate"
|
||||
OperationPortainerRegistryUpdateAccess Authorization = "PortainerRegistryUpdateAccess"
|
||||
OperationPortainerRegistryDelete Authorization = "PortainerRegistryDelete"
|
||||
OperationPortainerResourceControlCreate Authorization = "PortainerResourceControlCreate"
|
||||
OperationPortainerResourceControlUpdate Authorization = "PortainerResourceControlUpdate"
|
||||
OperationPortainerResourceControlDelete Authorization = "PortainerResourceControlDelete"
|
||||
OperationPortainerRoleList Authorization = "PortainerRoleList"
|
||||
OperationPortainerRoleInspect Authorization = "PortainerRoleInspect"
|
||||
OperationPortainerRoleCreate Authorization = "PortainerRoleCreate"
|
||||
OperationPortainerRoleUpdate Authorization = "PortainerRoleUpdate"
|
||||
OperationPortainerRoleDelete Authorization = "PortainerRoleDelete"
|
||||
OperationPortainerScheduleList Authorization = "PortainerScheduleList"
|
||||
OperationPortainerScheduleInspect Authorization = "PortainerScheduleInspect"
|
||||
OperationPortainerScheduleFile Authorization = "PortainerScheduleFile"
|
||||
OperationPortainerScheduleTasks Authorization = "PortainerScheduleTasks"
|
||||
OperationPortainerScheduleCreate Authorization = "PortainerScheduleCreate"
|
||||
OperationPortainerScheduleUpdate Authorization = "PortainerScheduleUpdate"
|
||||
OperationPortainerScheduleDelete Authorization = "PortainerScheduleDelete"
|
||||
OperationPortainerSettingsInspect Authorization = "PortainerSettingsInspect"
|
||||
OperationPortainerSettingsUpdate Authorization = "PortainerSettingsUpdate"
|
||||
OperationPortainerSettingsLDAPCheck Authorization = "PortainerSettingsLDAPCheck"
|
||||
OperationPortainerStackList Authorization = "PortainerStackList"
|
||||
OperationPortainerStackInspect Authorization = "PortainerStackInspect"
|
||||
OperationPortainerStackFile Authorization = "PortainerStackFile"
|
||||
OperationPortainerStackCreate Authorization = "PortainerStackCreate"
|
||||
OperationPortainerStackMigrate Authorization = "PortainerStackMigrate"
|
||||
OperationPortainerStackUpdate Authorization = "PortainerStackUpdate"
|
||||
OperationPortainerStackDelete Authorization = "PortainerStackDelete"
|
||||
OperationPortainerTagList Authorization = "PortainerTagList"
|
||||
OperationPortainerTagCreate Authorization = "PortainerTagCreate"
|
||||
OperationPortainerTagDelete Authorization = "PortainerTagDelete"
|
||||
OperationPortainerTeamMembershipList Authorization = "PortainerTeamMembershipList"
|
||||
OperationPortainerTeamMembershipCreate Authorization = "PortainerTeamMembershipCreate"
|
||||
OperationPortainerTeamMembershipUpdate Authorization = "PortainerTeamMembershipUpdate"
|
||||
OperationPortainerTeamMembershipDelete Authorization = "PortainerTeamMembershipDelete"
|
||||
OperationPortainerTeamList Authorization = "PortainerTeamList"
|
||||
OperationPortainerTeamInspect Authorization = "PortainerTeamInspect"
|
||||
OperationPortainerTeamMemberships Authorization = "PortainerTeamMemberships"
|
||||
OperationPortainerTeamCreate Authorization = "PortainerTeamCreate"
|
||||
OperationPortainerTeamUpdate Authorization = "PortainerTeamUpdate"
|
||||
OperationPortainerTeamDelete Authorization = "PortainerTeamDelete"
|
||||
OperationPortainerTemplateList Authorization = "PortainerTemplateList"
|
||||
OperationPortainerTemplateInspect Authorization = "PortainerTemplateInspect"
|
||||
OperationPortainerTemplateCreate Authorization = "PortainerTemplateCreate"
|
||||
OperationPortainerTemplateUpdate Authorization = "PortainerTemplateUpdate"
|
||||
OperationPortainerTemplateDelete Authorization = "PortainerTemplateDelete"
|
||||
OperationPortainerUploadTLS Authorization = "PortainerUploadTLS"
|
||||
OperationPortainerUserList Authorization = "PortainerUserList"
|
||||
OperationPortainerUserInspect Authorization = "PortainerUserInspect"
|
||||
OperationPortainerUserMemberships Authorization = "PortainerUserMemberships"
|
||||
OperationPortainerUserCreate Authorization = "PortainerUserCreate"
|
||||
OperationPortainerUserUpdate Authorization = "PortainerUserUpdate"
|
||||
OperationPortainerUserUpdatePassword Authorization = "PortainerUserUpdatePassword"
|
||||
OperationPortainerUserDelete Authorization = "PortainerUserDelete"
|
||||
OperationPortainerWebsocketExec Authorization = "PortainerWebsocketExec"
|
||||
OperationPortainerWebhookList Authorization = "PortainerWebhookList"
|
||||
OperationPortainerWebhookCreate Authorization = "PortainerWebhookCreate"
|
||||
OperationPortainerWebhookDelete Authorization = "PortainerWebhookDelete"
|
||||
|
||||
OperationDockerUndefined Authorization = "DockerUndefined"
|
||||
OperationDockerAgentUndefined Authorization = "DockerAgentUndefined"
|
||||
OperationPortainerUndefined Authorization = "PortainerUndefined"
|
||||
|
||||
EndpointResourcesAccess Authorization = "EndpointResourcesAccess"
|
||||
|
||||
// Deprecated operations
|
||||
OperationPortainerEndpointExtensionAdd Authorization = "PortainerEndpointExtensionAdd"
|
||||
OperationPortainerEndpointExtensionRemove Authorization = "PortainerEndpointExtensionRemove"
|
||||
OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -849,10 +849,6 @@ definitions:
|
||||
EdgeKey:
|
||||
description: The key which is used to map the agent to Portainer
|
||||
type: string
|
||||
Extensions:
|
||||
items:
|
||||
$ref: '#/definitions/portainer.EndpointExtension'
|
||||
type: array
|
||||
GroupId:
|
||||
description: Endpoint group identifier
|
||||
example: 1
|
||||
@@ -926,13 +922,6 @@ definitions:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/portainer.Authorizations'
|
||||
type: object
|
||||
portainer.EndpointExtension:
|
||||
properties:
|
||||
Type:
|
||||
type: integer
|
||||
URL:
|
||||
type: string
|
||||
type: object
|
||||
portainer.EndpointGroup:
|
||||
properties:
|
||||
AuthorizedTeams:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
|
||||
import { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 5 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('submit button should be disabled when name or image is missing', async () => {
|
||||
const user = new UserViewModel({ Username: 'user' });
|
||||
|
||||
const { findByText, getByText, getByLabelText } = renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<CreateContainerInstanceForm />
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(findByText(/Azure settings/)).resolves.toBeVisible();
|
||||
|
||||
const button = getByText(/Deploy the container/);
|
||||
expect(button).toBeVisible();
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
const nameInput = getByLabelText(/name/i);
|
||||
userEvent.type(nameInput, 'name');
|
||||
|
||||
const imageInput = getByLabelText(/image/i);
|
||||
userEvent.type(imageInput, 'image');
|
||||
|
||||
await expect(findByText(/Deploy the container/)).resolves.toBeEnabled();
|
||||
|
||||
expect(nameInput).toHaveValue('name');
|
||||
userEvent.clear(nameInput);
|
||||
|
||||
await expect(findByText(/Deploy the container/)).resolves.toBeDisabled();
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Input, Select } from '@/portainer/components/form-components/Input';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { InputListError } from '@/portainer/components/form-components/InputList/InputList';
|
||||
import { AccessControlForm } from '@/portainer/components/accessControlForm';
|
||||
import { ContainerInstanceFormValues } from '@/azure/types';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { isAdmin, useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { validationSchema } from './CreateContainerInstanceForm.validation';
|
||||
import { PortMapping, PortsMappingField } from './PortsMappingField';
|
||||
import { useLoadFormState } from './useLoadFormState';
|
||||
import {
|
||||
getSubscriptionLocations,
|
||||
getSubscriptionResourceGroups,
|
||||
} from './utils';
|
||||
import { useCreateInstance } from './useCreateInstanceMutation';
|
||||
|
||||
export function CreateContainerInstanceForm() {
|
||||
const {
|
||||
params: { endpointId: environmentId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
if (!environmentId) {
|
||||
throw new Error('endpointId url param is required');
|
||||
}
|
||||
|
||||
const { user } = useUser();
|
||||
const isUserAdmin = isAdmin(user);
|
||||
|
||||
const { initialValues, isLoading, providers, subscriptions, resourceGroups } =
|
||||
useLoadFormState(environmentId, isUserAdmin);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync } = useCreateInstance(
|
||||
resourceGroups,
|
||||
environmentId,
|
||||
user?.Id
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik<ContainerInstanceFormValues>
|
||||
initialValues={initialValues}
|
||||
validationSchema={() => validationSchema(isUserAdmin)}
|
||||
onSubmit={onSubmit}
|
||||
validateOnMount
|
||||
validateOnChange
|
||||
enableReinitialize
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => (
|
||||
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
|
||||
<FormSectionTitle>Azure settings</FormSectionTitle>
|
||||
<FormControl
|
||||
label="Subscription"
|
||||
inputId="subscription-input"
|
||||
errors={errors.subscription}
|
||||
>
|
||||
<Field
|
||||
name="subscription"
|
||||
as={Select}
|
||||
id="subscription-input"
|
||||
options={subscriptions}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Resource group"
|
||||
inputId="resourceGroup-input"
|
||||
errors={errors.resourceGroup}
|
||||
>
|
||||
<Field
|
||||
name="resourceGroup"
|
||||
as={Select}
|
||||
id="resourceGroup-input"
|
||||
options={getSubscriptionResourceGroups(
|
||||
values.subscription,
|
||||
resourceGroups
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Location"
|
||||
inputId="location-input"
|
||||
errors={errors.location}
|
||||
>
|
||||
<Field
|
||||
name="location"
|
||||
as={Select}
|
||||
id="location-input"
|
||||
options={getSubscriptionLocations(values.subscription, providers)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormSectionTitle>Container configuration</FormSectionTitle>
|
||||
|
||||
<FormControl label="Name" inputId="name-input" errors={errors.name}>
|
||||
<Field
|
||||
name="name"
|
||||
as={Input}
|
||||
id="name-input"
|
||||
placeholder="e.g. myContainer"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Image"
|
||||
inputId="image-input"
|
||||
errors={errors.image}
|
||||
>
|
||||
<Field
|
||||
name="image"
|
||||
as={Input}
|
||||
id="image-input"
|
||||
placeholder="e.g. nginx:alpine"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="OS" inputId="os-input" errors={errors.os}>
|
||||
<Field
|
||||
name="os"
|
||||
as={Select}
|
||||
id="os-input"
|
||||
options={[
|
||||
{ label: 'Linux', value: 'Linux' },
|
||||
{ label: 'Windows', value: 'Windows' },
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<PortsMappingField
|
||||
value={values.ports}
|
||||
onChange={(value) => setFieldValue('ports', value)}
|
||||
errors={errors.ports as InputListError<PortMapping>[]}
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12 small text-muted">
|
||||
This will automatically deploy a container with a public IP
|
||||
address
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormSectionTitle>Container Resources</FormSectionTitle>
|
||||
|
||||
<FormControl label="CPU" inputId="cpu-input" errors={errors.cpu}>
|
||||
<Field
|
||||
name="cpu"
|
||||
as={Input}
|
||||
id="cpu-input"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Memory"
|
||||
inputId="cpu-input"
|
||||
errors={errors.memory}
|
||||
>
|
||||
<Field
|
||||
name="memory"
|
||||
as={Input}
|
||||
id="memory-input"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<AccessControlForm
|
||||
formNamespace="accessControl"
|
||||
onChange={(values) => setFieldValue('accessControl', values)}
|
||||
values={values.accessControl}
|
||||
errors={errors.accessControl}
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Deployment in progress..."
|
||||
>
|
||||
<i className="fa fa-plus space-right" aria-hidden="true" />
|
||||
Deploy the container
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
|
||||
async function onSubmit(values: ContainerInstanceFormValues) {
|
||||
try {
|
||||
await mutateAsync(values);
|
||||
notifications.success('Container successfully created', values.name);
|
||||
router.stateService.go('azure.containerinstances');
|
||||
} catch (e) {
|
||||
notifications.error('Failure', e as Error, 'Unable to create container');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { object, string, number, boolean } from 'yup';
|
||||
|
||||
import { validationSchema as accessControlSchema } from '@/portainer/components/accessControlForm/AccessControlForm.validation';
|
||||
|
||||
import { validationSchema as portsSchema } from './PortsMappingField.validation';
|
||||
|
||||
export function validationSchema(isAdmin: boolean) {
|
||||
return object().shape({
|
||||
name: string().required('Name is required.'),
|
||||
image: string().required('Image is required.'),
|
||||
subscription: string().required('Subscription is required.'),
|
||||
resourceGroup: string().required('Resource group is required.'),
|
||||
location: string().required('Location is required.'),
|
||||
os: string().oneOf(['Linux', 'Windows']),
|
||||
cpu: number().positive(),
|
||||
memory: number().positive(),
|
||||
allocatePublicIP: boolean(),
|
||||
ports: portsSchema(),
|
||||
accessControl: accessControlSchema(isAdmin),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item .inputs {
|
||||
}
|
||||
|
||||
.item .errors {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ButtonSelector } from '@/portainer/components/form-components/ButtonSelector/ButtonSelector';
|
||||
import { FormError } from '@/portainer/components/form-components/FormError';
|
||||
import { InputGroup } from '@/portainer/components/form-components/InputGroup';
|
||||
import { InputList } from '@/portainer/components/form-components/InputList';
|
||||
import {
|
||||
InputListError,
|
||||
ItemProps,
|
||||
} from '@/portainer/components/form-components/InputList/InputList';
|
||||
|
||||
import styles from './PortsMappingField.module.css';
|
||||
|
||||
type Protocol = 'TCP' | 'UDP';
|
||||
|
||||
export interface PortMapping {
|
||||
host: string;
|
||||
container: string;
|
||||
protocol: Protocol;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: PortMapping[];
|
||||
onChange(value: PortMapping[]): void;
|
||||
errors?: InputListError<PortMapping>[] | string;
|
||||
}
|
||||
|
||||
export function PortsMappingField({ value, onChange, errors }: Props) {
|
||||
return (
|
||||
<>
|
||||
<InputList<PortMapping>
|
||||
label="Port mapping"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
addLabel="map additional port"
|
||||
itemBuilder={() => ({ host: '', container: '', protocol: 'TCP' })}
|
||||
item={Item}
|
||||
errors={errors}
|
||||
/>
|
||||
{typeof errors === 'string' && (
|
||||
<div className="form-group col-md-12">
|
||||
<FormError>{errors}</FormError>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({ onChange, item, error }: ItemProps<PortMapping>) {
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.inputs}>
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>host</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
placeholder="e.g. 80"
|
||||
value={item.host}
|
||||
onChange={(e) => handleChange('host', e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<span style={{ margin: '0 10px 0 10px' }}>
|
||||
<i className="fa fa-long-arrow-alt-right" aria-hidden="true" />
|
||||
</span>
|
||||
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>container</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
placeholder="e.g. 80"
|
||||
value={item.container}
|
||||
onChange={(e) => handleChange('container', e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<ButtonSelector<Protocol>
|
||||
onChange={(value) => handleChange('protocol', value)}
|
||||
value={item.protocol}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
</div>
|
||||
{!!error && (
|
||||
<div className={styles.errors}>
|
||||
<FormError>{Object.values(error)[0]}</FormError>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleChange(name: string, value: string) {
|
||||
onChange({ ...item, [name]: value });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { array, object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return array(
|
||||
object().shape({
|
||||
host: string().required('host is required'),
|
||||
container: string().required('container is required'),
|
||||
protocol: string().oneOf(['TCP', 'UDP']),
|
||||
})
|
||||
).min(1, 'At least one port binding is required');
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { createContainerGroup } from '@/azure/services/container-groups.service';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import {
|
||||
ContainerGroup,
|
||||
ContainerInstanceFormValues,
|
||||
ResourceGroup,
|
||||
} from '@/azure/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { applyResourceControl } from '@/portainer/resource-control/resource-control.service';
|
||||
|
||||
import { getSubscriptionResourceGroups } from './utils';
|
||||
|
||||
export function useCreateInstance(
|
||||
resourceGroups: {
|
||||
[k: string]: ResourceGroup[];
|
||||
},
|
||||
environmentId: EnvironmentId,
|
||||
userId?: UserId
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<ContainerGroup, unknown, ContainerInstanceFormValues>(
|
||||
(values) => {
|
||||
if (!values.subscription) {
|
||||
throw new PortainerError('subscription is required');
|
||||
}
|
||||
|
||||
const subscriptionResourceGroup = getSubscriptionResourceGroups(
|
||||
values.subscription,
|
||||
resourceGroups
|
||||
);
|
||||
const resourceGroup = subscriptionResourceGroup.find(
|
||||
(r) => r.value === values.resourceGroup
|
||||
);
|
||||
if (!resourceGroup) {
|
||||
throw new PortainerError('resource group not found');
|
||||
}
|
||||
|
||||
return createContainerGroup(
|
||||
values,
|
||||
environmentId,
|
||||
values.subscription,
|
||||
resourceGroup.label
|
||||
);
|
||||
},
|
||||
{
|
||||
async onSuccess(containerGroup, values) {
|
||||
if (!userId) {
|
||||
throw new Error('missing user id');
|
||||
}
|
||||
|
||||
const resourceControl = containerGroup.Portainer.ResourceControl;
|
||||
const accessControlData = values.accessControl;
|
||||
await applyResourceControl(userId, accessControlData, resourceControl);
|
||||
queryClient.invalidateQueries(['azure', 'container-instances']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useQueries, useQuery } from 'react-query';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import { Option } from '@/portainer/components/form-components/Input/Select';
|
||||
import { getResourceGroups } from '@/azure/services/resource-groups.service';
|
||||
import { getSubscriptions } from '@/azure/services/subscription.service';
|
||||
import { getContainerInstanceProvider } from '@/azure/services/provider.service';
|
||||
import { ContainerInstanceFormValues, Subscription } from '@/azure/types';
|
||||
import { parseFromResourceControl } from '@/portainer/components/accessControlForm/model';
|
||||
|
||||
import {
|
||||
getSubscriptionLocations,
|
||||
getSubscriptionResourceGroups,
|
||||
} from './utils';
|
||||
|
||||
export function useLoadFormState(
|
||||
environmentId: EnvironmentId,
|
||||
isUserAdmin: boolean
|
||||
) {
|
||||
const { subscriptions, isLoading: isLoadingSubscriptions } =
|
||||
useSubscriptions(environmentId);
|
||||
const { resourceGroups, isLoading: isLoadingResourceGroups } =
|
||||
useResourceGroups(environmentId, subscriptions);
|
||||
const { providers, isLoading: isLoadingProviders } = useProviders(
|
||||
environmentId,
|
||||
subscriptions
|
||||
);
|
||||
|
||||
const subscriptionOptions =
|
||||
subscriptions?.map((s) => ({
|
||||
value: s.subscriptionId,
|
||||
label: s.displayName,
|
||||
})) || [];
|
||||
|
||||
const initSubscriptionId = getFirstValue(subscriptionOptions);
|
||||
|
||||
const subscriptionResourceGroups = getSubscriptionResourceGroups(
|
||||
initSubscriptionId,
|
||||
resourceGroups
|
||||
);
|
||||
|
||||
const subscriptionLocations = getSubscriptionLocations(
|
||||
initSubscriptionId,
|
||||
providers
|
||||
);
|
||||
|
||||
const initialValues: ContainerInstanceFormValues = {
|
||||
name: '',
|
||||
location: getFirstValue(subscriptionLocations),
|
||||
subscription: initSubscriptionId,
|
||||
resourceGroup: getFirstValue(subscriptionResourceGroups),
|
||||
image: '',
|
||||
os: 'Linux',
|
||||
memory: 1,
|
||||
cpu: 1,
|
||||
ports: [{ container: '80', host: '80', protocol: 'TCP' }],
|
||||
allocatePublicIP: true,
|
||||
accessControl: parseFromResourceControl(isUserAdmin),
|
||||
};
|
||||
|
||||
return {
|
||||
isUserAdmin,
|
||||
initialValues,
|
||||
subscriptions: subscriptionOptions,
|
||||
resourceGroups,
|
||||
providers,
|
||||
isLoading:
|
||||
isLoadingProviders || isLoadingResourceGroups || isLoadingSubscriptions,
|
||||
};
|
||||
|
||||
function getFirstValue<T extends string | number>(arr: Option<T>[]) {
|
||||
if (arr.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return arr[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
function useSubscriptions(environmentId: EnvironmentId) {
|
||||
const { data, isError, error, isLoading } = useQuery(
|
||||
'azure.subscriptions',
|
||||
() => getSubscriptions(environmentId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return { subscriptions: data || [], isLoading };
|
||||
}
|
||||
|
||||
function useResourceGroups(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptions: Subscription[]
|
||||
) {
|
||||
const queries = useQueries(
|
||||
subscriptions.map((subscription) => ({
|
||||
queryKey: ['azure.resourceGroups', subscription.subscriptionId],
|
||||
queryFn: () =>
|
||||
getResourceGroups(environmentId, subscription.subscriptionId),
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const failedQuery = queries.find((q) => q.error);
|
||||
if (failedQuery) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
failedQuery.error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [queries]);
|
||||
|
||||
return {
|
||||
resourceGroups: Object.fromEntries(
|
||||
queries.map((q, index) => [
|
||||
subscriptions[index].subscriptionId,
|
||||
q.data || [],
|
||||
])
|
||||
),
|
||||
isLoading: queries.some((q) => q.isLoading),
|
||||
};
|
||||
}
|
||||
|
||||
function useProviders(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptions: Subscription[]
|
||||
) {
|
||||
const queries = useQueries(
|
||||
subscriptions.map((subscription) => ({
|
||||
queryKey: [
|
||||
'azure.containerInstanceProvider',
|
||||
subscription.subscriptionId,
|
||||
],
|
||||
queryFn: () =>
|
||||
getContainerInstanceProvider(
|
||||
environmentId,
|
||||
subscription.subscriptionId
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const failedQuery = queries.find((q) => q.error);
|
||||
if (failedQuery) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
failedQuery.error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [queries]);
|
||||
|
||||
return {
|
||||
providers: Object.fromEntries(
|
||||
queries.map((q, index) => [subscriptions[index].subscriptionId, q.data])
|
||||
),
|
||||
isLoading: queries.some((q) => q.isLoading),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ProviderViewModel } from '@/azure/models/provider';
|
||||
import { ResourceGroup } from '@/azure/types';
|
||||
|
||||
export function getSubscriptionResourceGroups(
|
||||
subscriptionId?: string,
|
||||
resourceGroups?: Record<string, ResourceGroup[]>
|
||||
) {
|
||||
if (!subscriptionId || !resourceGroups || !resourceGroups[subscriptionId]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return resourceGroups[subscriptionId].map(({ name, id }) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getSubscriptionLocations(
|
||||
subscriptionId?: string,
|
||||
containerInstanceProviders?: Record<string, ProviderViewModel | undefined>
|
||||
) {
|
||||
if (!subscriptionId || !containerInstanceProviders) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const provider = containerInstanceProviders[subscriptionId];
|
||||
if (!provider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return provider.locations.map((location) => ({
|
||||
value: location,
|
||||
label: location,
|
||||
}));
|
||||
}
|
||||
34
app/azure/ContainerInstances/CreateContainerInstanceView.tsx
Normal file
34
app/azure/ContainerInstances/CreateContainerInstanceView.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||
import { Widget, WidgetBody } from '@/portainer/components/widget';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
import { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
|
||||
|
||||
export function CreateContainerInstanceView() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create container instance"
|
||||
breadcrumbs={[
|
||||
{ link: 'azure.containerinstances', label: 'Container instances' },
|
||||
{ label: 'Add container' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<CreateContainerInstanceForm />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const CreateContainerInstanceViewAngular = r2a(
|
||||
CreateContainerInstanceView,
|
||||
[]
|
||||
);
|
||||
11
app/azure/ContainerInstances/index.ts
Normal file
11
app/azure/ContainerInstances/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { CreateContainerInstanceViewAngular } from './CreateContainerInstanceView';
|
||||
|
||||
export const containerInstancesModule = angular
|
||||
.module('portainer.azure.containerInstances', [])
|
||||
|
||||
.component(
|
||||
'createContainerInstanceView',
|
||||
CreateContainerInstanceViewAngular
|
||||
).name;
|
||||
@@ -1,4 +1,8 @@
|
||||
angular.module('portainer.azure', ['portainer.app']).config([
|
||||
import angular from 'angular';
|
||||
|
||||
import { containerInstancesModule } from './ContainerInstances';
|
||||
|
||||
angular.module('portainer.azure', ['portainer.app', containerInstancesModule]).config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
@@ -18,7 +22,7 @@ angular.module('portainer.azure', ['portainer.app']).config([
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
||||
await StateManager.updateEndpointState(endpoint, []);
|
||||
await StateManager.updateEndpointState(endpoint);
|
||||
} catch (e) {
|
||||
Notifications.error('Failed loading environment', e);
|
||||
$state.go('portainer.home', {}, { reload: true });
|
||||
@@ -53,8 +57,7 @@ angular.module('portainer.azure', ['portainer.app']).config([
|
||||
url: '/new/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/containerinstances/create/createcontainerinstance.html',
|
||||
controller: 'AzureCreateContainerInstanceController',
|
||||
component: 'createContainerInstanceView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,48 +48,3 @@ export function ContainerGroupViewModel(data) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateContainerGroupRequest(model) {
|
||||
this.location = model.Location;
|
||||
|
||||
var containerPorts = [];
|
||||
var addressPorts = [];
|
||||
for (var i = 0; i < model.Ports.length; i++) {
|
||||
var binding = model.Ports[i];
|
||||
if (!binding.container || !binding.host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
containerPorts.push({
|
||||
port: binding.container,
|
||||
});
|
||||
|
||||
addressPorts.push({
|
||||
port: binding.host,
|
||||
protocol: binding.protocol,
|
||||
});
|
||||
}
|
||||
|
||||
this.properties = {
|
||||
osType: model.OSType,
|
||||
containers: [
|
||||
{
|
||||
name: model.Name,
|
||||
properties: {
|
||||
image: model.Image,
|
||||
ports: containerPorts,
|
||||
resources: {
|
||||
requests: {
|
||||
cpu: model.CPU,
|
||||
memoryInGB: model.Memory,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
ipAddress: {
|
||||
type: model.AllocatePublicIP ? 'Public' : 'Private',
|
||||
ports: addressPorts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
export function ContainerInstanceProviderViewModel(data) {
|
||||
this.Id = data.id;
|
||||
this.Namespace = data.namespace;
|
||||
|
||||
var containerGroupType = _.find(data.resourceTypes, { resourceType: 'containerGroups' });
|
||||
this.Locations = containerGroupType.locations;
|
||||
}
|
||||
21
app/azure/models/provider.ts
Normal file
21
app/azure/models/provider.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { ProviderResponse } from '../types';
|
||||
|
||||
export interface ProviderViewModel {
|
||||
id: string;
|
||||
namespace: string;
|
||||
locations: string[];
|
||||
}
|
||||
|
||||
export function parseViewModel({
|
||||
id,
|
||||
namespace,
|
||||
resourceTypes,
|
||||
}: ProviderResponse): ProviderViewModel {
|
||||
const containerGroupType = _.find(resourceTypes, {
|
||||
resourceType: 'containerGroups',
|
||||
});
|
||||
const { locations = [] } = containerGroupType || {};
|
||||
return { id, namespace, locations };
|
||||
}
|
||||
@@ -11,7 +11,6 @@ angular.module('portainer.azure').factory('Subscription', [
|
||||
'api-version': '2016-06-01',
|
||||
},
|
||||
{
|
||||
query: { method: 'GET' },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,72 +1,75 @@
|
||||
angular.module('portainer.azure').factory('AzureService', [
|
||||
'$q',
|
||||
'Azure',
|
||||
'SubscriptionService',
|
||||
'ResourceGroupService',
|
||||
'ContainerGroupService',
|
||||
'ProviderService',
|
||||
function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupService, ContainerGroupService, ProviderService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
import { ResourceGroupViewModel } from '../models/resource_group';
|
||||
import { SubscriptionViewModel } from '../models/subscription';
|
||||
import { getResourceGroups } from './resource-groups.service';
|
||||
import { getSubscriptions } from './subscription.service';
|
||||
|
||||
service.deleteContainerGroup = function (id) {
|
||||
return Azure.delete(id, '2018-04-01');
|
||||
};
|
||||
angular.module('portainer.azure').factory('AzureService', AzureService);
|
||||
|
||||
service.createContainerGroup = function (model, subscriptionId, resourceGroupName) {
|
||||
return ContainerGroupService.create(model, subscriptionId, resourceGroupName);
|
||||
};
|
||||
/* @ngInject */
|
||||
export function AzureService($q, Azure, $async, EndpointProvider, ContainerGroupService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.subscriptions = function () {
|
||||
return SubscriptionService.subscriptions();
|
||||
};
|
||||
service.deleteContainerGroup = function (id) {
|
||||
return Azure.delete(id, '2018-04-01');
|
||||
};
|
||||
|
||||
service.containerInstanceProvider = function (subscriptions) {
|
||||
return retrieveResourcesForEachSubscription(subscriptions, ProviderService.containerInstanceProvider);
|
||||
};
|
||||
service.subscriptions = async function subscriptions() {
|
||||
return $async(async () => {
|
||||
const environmentId = EndpointProvider.endpointID();
|
||||
const subscriptions = await getSubscriptions(environmentId);
|
||||
return subscriptions.map((s) => new SubscriptionViewModel(s));
|
||||
});
|
||||
};
|
||||
|
||||
service.resourceGroups = function (subscriptions) {
|
||||
return retrieveResourcesForEachSubscription(subscriptions, ResourceGroupService.resourceGroups);
|
||||
};
|
||||
service.resourceGroups = function resourceGroups(subscriptions) {
|
||||
return $async(async () => {
|
||||
return retrieveResourcesForEachSubscription(subscriptions, async (subscriptionId) => {
|
||||
const environmentId = EndpointProvider.endpointID();
|
||||
|
||||
service.containerGroups = function (subscriptions) {
|
||||
return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups);
|
||||
};
|
||||
|
||||
service.aggregate = function (resourcesBySubcription) {
|
||||
var aggregatedResources = [];
|
||||
Object.keys(resourcesBySubcription).forEach(function (key) {
|
||||
aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]);
|
||||
const resourceGroups = await getResourceGroups(environmentId, subscriptionId);
|
||||
return resourceGroups.map((r) => new ResourceGroupViewModel(r, subscriptionId));
|
||||
});
|
||||
return aggregatedResources;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) {
|
||||
var deferred = $q.defer();
|
||||
service.containerGroups = function (subscriptions) {
|
||||
return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups);
|
||||
};
|
||||
|
||||
var resources = {};
|
||||
service.aggregate = function (resourcesBySubscription) {
|
||||
var aggregatedResources = [];
|
||||
Object.keys(resourcesBySubscription).forEach(function (key) {
|
||||
aggregatedResources = aggregatedResources.concat(resourcesBySubscription[key]);
|
||||
});
|
||||
return aggregatedResources;
|
||||
};
|
||||
|
||||
var resourceQueries = [];
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
var subscription = subscriptions[i];
|
||||
resourceQueries.push(resourceQuery(subscription.Id));
|
||||
}
|
||||
function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$q.all(resourceQueries)
|
||||
.then(function success(data) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var result = data[i];
|
||||
resources[subscriptions[i].Id] = result;
|
||||
}
|
||||
deferred.resolve(resources);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve resources', err: err });
|
||||
});
|
||||
var resources = {};
|
||||
|
||||
return deferred.promise;
|
||||
var resourceQueries = [];
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
var subscription = subscriptions[i];
|
||||
resourceQueries.push(resourceQuery(subscription.Id));
|
||||
}
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
$q.all(resourceQueries)
|
||||
.then(function success(data) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var result = data[i];
|
||||
resources[subscriptions[i].Id] = result;
|
||||
}
|
||||
deferred.resolve(resources);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve resources', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
78
app/azure/services/container-groups.service.ts
Normal file
78
app/azure/services/container-groups.service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { ContainerGroup, ContainerInstanceFormValues } from '../types';
|
||||
|
||||
export async function createContainerGroup(
|
||||
model: ContainerInstanceFormValues,
|
||||
environmentId: EnvironmentId,
|
||||
subscriptionId: string,
|
||||
resourceGroupName: string
|
||||
) {
|
||||
const payload = transformToPayload(model);
|
||||
try {
|
||||
const { data } = await axios.put<ContainerGroup>(
|
||||
buildUrl(environmentId, subscriptionId, resourceGroupName, model.name),
|
||||
payload,
|
||||
{ params: { 'api-version': '2018-04-01' } }
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptionId: string,
|
||||
resourceGroupName: string,
|
||||
containerGroupName: string
|
||||
) {
|
||||
return `/endpoints/${environmentId}/azure/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ContainerInstance/containerGroups/${containerGroupName}`;
|
||||
}
|
||||
|
||||
function transformToPayload(model: ContainerInstanceFormValues) {
|
||||
const containerPorts = [];
|
||||
const addressPorts = [];
|
||||
|
||||
const ports = model.ports.filter((p) => p.container && p.host);
|
||||
|
||||
for (let i = 0; i < ports.length; i += 1) {
|
||||
const binding = ports[i];
|
||||
|
||||
containerPorts.push({
|
||||
port: binding.container,
|
||||
});
|
||||
|
||||
addressPorts.push({
|
||||
port: binding.host,
|
||||
protocol: binding.protocol,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
location: model.location,
|
||||
properties: {
|
||||
osType: model.os,
|
||||
containers: [
|
||||
{
|
||||
name: model.name,
|
||||
properties: {
|
||||
image: model.image,
|
||||
ports: containerPorts,
|
||||
resources: {
|
||||
requests: {
|
||||
cpu: model.cpu,
|
||||
memoryInGB: model.memory,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
ipAddress: {
|
||||
type: model.allocatePublicIP ? 'Public' : 'Private',
|
||||
ports: addressPorts,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContainerGroupViewModel, CreateContainerGroupRequest } from '../models/container_group';
|
||||
import { ContainerGroupViewModel } from '../models/container_group';
|
||||
|
||||
angular.module('portainer.azure').factory('ContainerGroupService', [
|
||||
'$q',
|
||||
@@ -30,18 +30,6 @@ angular.module('portainer.azure').factory('ContainerGroupService', [
|
||||
return new ContainerGroupViewModel(containerGroup);
|
||||
}
|
||||
|
||||
service.create = function (model, subscriptionId, resourceGroupName) {
|
||||
var payload = new CreateContainerGroupRequest(model);
|
||||
return ContainerGroup.create(
|
||||
{
|
||||
subscriptionId: subscriptionId,
|
||||
resourceGroupName: resourceGroupName,
|
||||
containerGroupName: model.Name,
|
||||
},
|
||||
payload
|
||||
).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
||||
29
app/azure/services/provider.service.ts
Normal file
29
app/azure/services/provider.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// import { ContainerInstanceProviderViewModel } from '../models/provider';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { parseViewModel } from '../models/provider';
|
||||
import { ProviderResponse } from '../types';
|
||||
|
||||
import { azureErrorParser } from './utils';
|
||||
|
||||
export async function getContainerInstanceProvider(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptionId: string
|
||||
) {
|
||||
try {
|
||||
const url = `/endpoints/${environmentId}/azure/subscriptions/${subscriptionId}/providers/Microsoft.ContainerInstance`;
|
||||
const { data } = await axios.get<ProviderResponse>(url, {
|
||||
params: { 'api-version': '2018-02-01' },
|
||||
});
|
||||
|
||||
return parseViewModel(data);
|
||||
} catch (error) {
|
||||
throw parseAxiosError(
|
||||
error as Error,
|
||||
'Unable to retrieve provider',
|
||||
azureErrorParser
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ContainerInstanceProviderViewModel } from '../models/provider';
|
||||
|
||||
angular.module('portainer.azure').factory('ProviderService', [
|
||||
'$q',
|
||||
'Provider',
|
||||
function ProviderServiceFactory($q, Provider) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.containerInstanceProvider = function (subscriptionId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Provider.get({ subscriptionId: subscriptionId, providerNamespace: 'Microsoft.ContainerInstance' })
|
||||
.$promise.then(function success(data) {
|
||||
var provider = new ContainerInstanceProviderViewModel(data);
|
||||
deferred.resolve(provider);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve provider', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
42
app/azure/services/resource-groups.service.ts
Normal file
42
app/azure/services/resource-groups.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { ResourceGroup } from '../types';
|
||||
|
||||
import { azureErrorParser } from './utils';
|
||||
|
||||
export async function getResourceGroups(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptionId: string
|
||||
) {
|
||||
try {
|
||||
const {
|
||||
data: { value },
|
||||
} = await axios.get<{ value: ResourceGroup[] }>(
|
||||
buildUrl(environmentId, subscriptionId),
|
||||
{ params: { 'api-version': '2018-02-01' } }
|
||||
);
|
||||
|
||||
return value;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(
|
||||
err as Error,
|
||||
'Unable to retrieve resource groups',
|
||||
azureErrorParser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptionId: string,
|
||||
resourceGroupName?: string
|
||||
) {
|
||||
let url = `/endpoints/${environmentId}/azure/subscriptions/${subscriptionId}/resourcegroups`;
|
||||
|
||||
if (resourceGroupName) {
|
||||
url += `/${resourceGroupName}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -7,23 +7,6 @@ angular.module('portainer.azure').factory('ResourceGroupService', [
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.resourceGroups = function (subscriptionId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
ResourceGroup.query({ subscriptionId: subscriptionId })
|
||||
.$promise.then(function success(data) {
|
||||
var resourceGroups = data.value.map(function (item) {
|
||||
return new ResourceGroupViewModel(item, subscriptionId);
|
||||
});
|
||||
deferred.resolve(resourceGroups);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve resource groups', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.resourceGroup = resourceGroup;
|
||||
async function resourceGroup(subscriptionId, resourceGroupName) {
|
||||
const group = await ResourceGroup.get({ subscriptionId, resourceGroupName }).$promise;
|
||||
|
||||
30
app/azure/services/subscription.service.ts
Normal file
30
app/azure/services/subscription.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { Subscription } from '../types';
|
||||
|
||||
import { azureErrorParser } from './utils';
|
||||
|
||||
export async function getSubscriptions(environmentId: EnvironmentId) {
|
||||
try {
|
||||
const { data } = await axios.get<{ value: Subscription[] }>(
|
||||
buildUrl(environmentId)
|
||||
);
|
||||
return data.value;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to retrieve subscriptions',
|
||||
azureErrorParser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(environmentId: EnvironmentId, id?: string) {
|
||||
let url = `/endpoints/${environmentId}/azure/subscriptions?api-version=2016-06-01`;
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -4,32 +4,11 @@ angular.module('portainer.azure').factory('SubscriptionService', [
|
||||
'$q',
|
||||
'Subscription',
|
||||
function SubscriptionServiceFactory($q, Subscription) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
return { subscription };
|
||||
|
||||
service.subscriptions = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Subscription.query({})
|
||||
.$promise.then(function success(data) {
|
||||
var subscriptions = data.value.map(function (item) {
|
||||
return new SubscriptionViewModel(item);
|
||||
});
|
||||
deferred.resolve(subscriptions);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve subscriptions', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.subscription = subscription;
|
||||
async function subscription(id) {
|
||||
const subscription = await Subscription.get({ id }).$promise;
|
||||
return new SubscriptionViewModel(subscription);
|
||||
}
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user