Compare commits
20 Commits
fix/EE-243
...
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 |
@@ -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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -23,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"
|
||||
@@ -118,13 +119,20 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
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 {
|
||||
log.Fatalf("Failed updating settings from flags: %v", err)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 @@ import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
import * as envVarsUtils from '@/portainer/helpers/env-vars';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
|
||||
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { ContainerDetailsViewModel } from '../../../models/container';
|
||||
@@ -65,7 +66,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
$scope.create = create;
|
||||
$scope.update = update;
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
Console: 'none',
|
||||
|
||||
@@ -65,6 +65,28 @@
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
</div>
|
||||
<!-- create-webhook -->
|
||||
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Create a container webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Create a webhook (or callback URI) to automate the recreate this container. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and recreate this container."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch box-selector-item limited business" style="margin-left: 20px">
|
||||
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" />
|
||||
<i class="orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
</label>
|
||||
<be-feature-indicator feature="containerWebhookFeature"></be-feature-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !create-webhook -->
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Network ports configuration </div>
|
||||
<!-- publish-exposed-ports -->
|
||||
<div class="form-group">
|
||||
|
||||
@@ -110,6 +110,19 @@
|
||||
<td>Finished</td>
|
||||
<td>{{ container.State.FinishedAt | getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="isAdmin && displayRecreateButton && applicationState.endpoint.type !== 4">
|
||||
<td colspan="1">
|
||||
Container webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Webhook (or callback URI) used to automate the recreate this container. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and recreate this container."
|
||||
></portainer-tooltip>
|
||||
<label class="switch box-selector-item limited business" style="margin-left: 20px">
|
||||
<input disable-authorization="DockerContainerUpdate" type="checkbox" ng-model="WebhookExists" disabled="disabled" ng-checked="true" /><i></i>
|
||||
</label>
|
||||
<be-feature-indicator feature="containerWebhookFeature"></be-feature-indicator>
|
||||
</td>
|
||||
</tr>
|
||||
<tr authorization="DockerContainerLogs, DockerContainerInspect, DockerContainerStats, DockerExecStart">
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
|
||||
angular.module('portainer.docker').controller('ContainerController', [
|
||||
'$q',
|
||||
@@ -49,6 +50,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.displayRecreateButton = false;
|
||||
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
||||
|
||||
$scope.config = {
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
|
||||
@@ -206,6 +206,7 @@ export function EdgeDevicesDatatable({
|
||||
<RowProvider
|
||||
key={key}
|
||||
disableTrustOnFirstConnect={disableTrustOnFirstConnect}
|
||||
isOpenAmtEnabled={isOpenAmtEnabled}
|
||||
>
|
||||
<TableRow<Environment>
|
||||
cells={row.cells}
|
||||
|
||||
@@ -2,21 +2,24 @@ import { createContext, useContext, useMemo, PropsWithChildren } from 'react';
|
||||
|
||||
interface RowContextState {
|
||||
disableTrustOnFirstConnect: boolean;
|
||||
isOpenAmtEnabled: boolean;
|
||||
}
|
||||
|
||||
const RowContext = createContext<RowContextState | null>(null);
|
||||
|
||||
export interface RowProviderProps {
|
||||
disableTrustOnFirstConnect: boolean;
|
||||
isOpenAmtEnabled: boolean;
|
||||
}
|
||||
|
||||
export function RowProvider({
|
||||
disableTrustOnFirstConnect,
|
||||
isOpenAmtEnabled,
|
||||
children,
|
||||
}: PropsWithChildren<RowProviderProps>) {
|
||||
const state = useMemo(
|
||||
() => ({ disableTrustOnFirstConnect }),
|
||||
[disableTrustOnFirstConnect]
|
||||
() => ({ disableTrustOnFirstConnect, isOpenAmtEnabled }),
|
||||
[disableTrustOnFirstConnect, isOpenAmtEnabled]
|
||||
);
|
||||
|
||||
return <RowContext.Provider value={state}>{children}</RowContext.Provider>;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CellProps, Column, TableInstance } from 'react-table';
|
||||
import { CellProps, Column } from 'react-table';
|
||||
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
import { ExpandingCell } from '@/portainer/components/datatables/components/ExpandingCell';
|
||||
import { useRowContext } from '@/edge/devices/components/EdgeDevicesDatatable/columns/RowContext';
|
||||
|
||||
export const name: Column<Environment> = {
|
||||
Header: 'Name',
|
||||
@@ -15,9 +16,15 @@ export const name: Column<Environment> = {
|
||||
sortType: 'string',
|
||||
};
|
||||
|
||||
export function NameCell({ value: name, row }: CellProps<TableInstance>) {
|
||||
export function NameCell({ value: name, row }: CellProps<Environment>) {
|
||||
const { isOpenAmtEnabled } = useRowContext();
|
||||
const showExpandedRow = !!(
|
||||
isOpenAmtEnabled &&
|
||||
row.original.AMTDeviceGUID &&
|
||||
row.original.AMTDeviceGUID.length > 0
|
||||
);
|
||||
return (
|
||||
<ExpandingCell row={row}>
|
||||
<ExpandingCell row={row} showExpandArrow={showExpandedRow}>
|
||||
<Link
|
||||
to="portainer.endpoints.endpoint"
|
||||
params={{ id: row.original.Id }}
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
}
|
||||
|
||||
.table-actions-menu-list {
|
||||
padding: 0 10px 0 10px;
|
||||
background: var(--bg-widget-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.table-actions-menu-list [data-reach-menu-item] {
|
||||
padding: 5px 5px !important;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.table-actions-menu-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
[data-reach-menu-link] {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.table-actions-title {
|
||||
color: var(--blue-2);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.expand-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Row, TableInstance } from 'react-table';
|
||||
import { Row } from 'react-table';
|
||||
|
||||
interface Props {
|
||||
row: Row<TableInstance>;
|
||||
import styles from './ExpandingCell.module.css';
|
||||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||
row: Row<D>;
|
||||
showExpandArrow: boolean;
|
||||
}
|
||||
|
||||
export function ExpandingCell({ children, row }: PropsWithChildren<Props>) {
|
||||
export function ExpandingCell<
|
||||
D extends Record<string, unknown> = Record<string, unknown>
|
||||
>({ row, showExpandArrow, children }: PropsWithChildren<Props<D>>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...row.getToggleRowExpandedProps()}>
|
||||
<i
|
||||
className={`fas ${arrowClass(row.isExpanded)} space-right`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<>
|
||||
{showExpandArrow && (
|
||||
<button type="button" className={styles.expandButton}>
|
||||
<i
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...row.getToggleRowExpandedProps()}
|
||||
className={`fas ${arrowClass(row.isExpanded)} space-right`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
function arrowClass(isExpanded: boolean) {
|
||||
|
||||
@@ -10,6 +10,7 @@ class GitFormAutoUpdateFieldsetController {
|
||||
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
|
||||
|
||||
this.limitedFeature = FeatureId.FORCE_REDEPLOYMENT;
|
||||
this.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
|
||||
}
|
||||
|
||||
copyWebhook() {
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.model.ShowForcePullImage && $ctrl.model.RepositoryAutomaticUpdates">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field name="forcePullImage" feature-id="$ctrl.stackPullImageFeature" checked="$ctrl.model.ForcePullImage" label="'Pull latest image'"> </por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
class StackRedeployGitFormController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
||||
constructor($async, $state, $compile, $scope, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$compile = $compile;
|
||||
this.$scope = $scope;
|
||||
this.StackService = StackService;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
this.WebhookHelper = WebhookHelper;
|
||||
this.FormHelper = FormHelper;
|
||||
|
||||
$scope.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
|
||||
this.state = {
|
||||
inProgress: false,
|
||||
redeployInProgress: false,
|
||||
@@ -31,6 +34,7 @@ class StackRedeployGitFormController {
|
||||
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||
RepositoryFetchInterval: '5m',
|
||||
RepositoryWebhookURL: '',
|
||||
ShowForcePullImage: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -86,27 +90,21 @@ class StackRedeployGitFormController {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
return this.$async(async () => {
|
||||
const tplCrop =
|
||||
'<div>Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.</div>' +
|
||||
'<div"><div style="position: absolute; right: 110px; top: 68px; z-index: 999">' +
|
||||
'<be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div></div>';
|
||||
const template = angular.element(tplCrop);
|
||||
const html = this.$compile(template)(this.$scope);
|
||||
this.ModalService.confirmStackUpdate(html, true, true, 'btn-warning', function (result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const confirmed = await this.ModalService.confirmAsync({
|
||||
title: 'Are you sure?',
|
||||
message: 'Any changes to this stack or application made locally in Portainer will be overridden, which may cause service interruption.',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Update',
|
||||
className: 'btn-warning',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.redeployInProgress = true;
|
||||
|
||||
await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
|
||||
this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
|
||||
this.Notifications.success('Pulled and redeployed stack successfully');
|
||||
await this.$state.reload();
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed redeploying stack');
|
||||
} finally {
|
||||
@@ -154,6 +152,7 @@ class StackRedeployGitFormController {
|
||||
// Init auto update
|
||||
if (this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)) {
|
||||
this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true;
|
||||
this.formValues.AutoUpdate.ShowForcePullImage = this.stack.Type !== 3;
|
||||
|
||||
if (this.stack.AutoUpdate.Interval) {
|
||||
this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.INTERVAL;
|
||||
|
||||
@@ -16,12 +16,26 @@ function TemplateListController($async, $state, DatatableService, Notifications,
|
||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||
};
|
||||
|
||||
this.filterByTemplateType = function (item) {
|
||||
switch (item.Type) {
|
||||
case 1: // container
|
||||
return ctrl.state.showContainerTemplates;
|
||||
case 2: // swarm stack
|
||||
return ctrl.showSwarmStacks;
|
||||
case 3: // docker compose
|
||||
return !ctrl.showSwarmStacks || (ctrl.showSwarmStacks && ctrl.state.showContainerTemplates);
|
||||
case 4: // Edge stack templates
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.updateCategories = function () {
|
||||
var availableCategories = [];
|
||||
|
||||
for (var i = 0; i < ctrl.templates.length; i++) {
|
||||
var template = ctrl.templates[i];
|
||||
if ((template.Type === 1 && ctrl.state.showContainerTemplates) || (template.Type === 2 && ctrl.showSwarmStacks) || (template.Type === 3 && !ctrl.showSwarmStacks)) {
|
||||
if (this.filterByTemplateType(template)) {
|
||||
availableCategories = availableCategories.concat(template.Categories);
|
||||
}
|
||||
}
|
||||
@@ -37,13 +51,6 @@ function TemplateListController($async, $state, DatatableService, Notifications,
|
||||
return _.includes(item.Categories, ctrl.state.selectedCategory);
|
||||
};
|
||||
|
||||
this.filterByType = function (item) {
|
||||
if ((item.Type === 1 && ctrl.state.showContainerTemplates) || (item.Type === 2 && ctrl.showSwarmStacks) || (item.Type === 3 && !ctrl.showSwarmStacks)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.duplicateTemplate = duplicateTemplate.bind(this);
|
||||
this.duplicateTemplateAsync = duplicateTemplateAsync.bind(this);
|
||||
function duplicateTemplate(template) {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<div class="blocklist">
|
||||
<template-item
|
||||
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
||||
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByTemplateType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
||||
model="template"
|
||||
type-label="{{ template.Type === 1 ? 'container' : 'stack' }}"
|
||||
on-select="($ctrl.selectAction)"
|
||||
@@ -55,7 +55,10 @@
|
||||
</template-item-actions>
|
||||
</template-item>
|
||||
<div ng-if="!$ctrl.templates" class="text-center text-muted"> Loading... </div>
|
||||
<div ng-if="($ctrl.templates | filter: $ctrl.filterByType | filter: $ctrl.filterByCategory | filter: $ctrl.state.textFilter).length === 0" class="text-center text-muted">
|
||||
<div
|
||||
ng-if="($ctrl.templates | filter: $ctrl.filterByTemplateType | filter: $ctrl.filterByCategory | filter: $ctrl.state.textFilter).length === 0"
|
||||
class="text-center text-muted"
|
||||
>
|
||||
No templates available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,4 +23,7 @@ export enum FeatureId {
|
||||
ACTIVITY_AUDIT = 'activity-audit',
|
||||
FORCE_REDEPLOYMENT = 'force-redeployment',
|
||||
HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window',
|
||||
STACK_PULL_IMAGE = 'stack-pull-image',
|
||||
STACK_WEBHOOK = 'stack-webhook',
|
||||
CONTAINER_WEBHOOK = 'container-webhook',
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ export async function init(edition: Edition) {
|
||||
[FeatureId.TEAM_MEMBERSHIP]: Edition.BE,
|
||||
[FeatureId.FORCE_REDEPLOYMENT]: Edition.BE,
|
||||
[FeatureId.HIDE_AUTO_UPDATE_WINDOW]: Edition.BE,
|
||||
[FeatureId.STACK_PULL_IMAGE]: Edition.BE,
|
||||
[FeatureId.STACK_WEBHOOK]: Edition.BE,
|
||||
[FeatureId.CONTAINER_WEBHOOK]: Edition.BE,
|
||||
};
|
||||
|
||||
state.currentEdition = currentEdition;
|
||||
|
||||
16
app/portainer/feature-flags/feature-ids.js
Normal file
16
app/portainer/feature-flags/feature-ids.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota';
|
||||
export const K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota';
|
||||
export const RBAC_ROLES = 'rbac-roles';
|
||||
export const REGISTRY_MANAGEMENT = 'registry-management';
|
||||
export const K8S_SETUP_DEFAULT = 'k8s-setup-default';
|
||||
export const S3_BACKUP_SETTING = 's3-backup-setting';
|
||||
export const HIDE_INTERNAL_AUTHENTICATION_PROMPT = 'hide-internal-authentication-prompt';
|
||||
export const TEAM_MEMBERSHIP = 'team-membership';
|
||||
export const HIDE_INTERNAL_AUTH = 'hide-internal-auth';
|
||||
export const EXTERNAL_AUTH_LDAP = 'external-auth-ldap';
|
||||
export const ACTIVITY_AUDIT = 'activity-audit';
|
||||
export const HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window';
|
||||
export const FORCE_REDEPLOYMENT = 'force-redeployment';
|
||||
export const STACK_PULL_IMAGE = 'stack-pull-image';
|
||||
export const STACK_WEBHOOK = 'stack-webhook';
|
||||
export const CONTAINER_WEBHOOK = 'container-webhook';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
confirmContainerDeletion,
|
||||
confirmContainerRecreation,
|
||||
confirmServiceForceUpdate,
|
||||
confirmStackUpdate,
|
||||
confirmKubeconfigSelection,
|
||||
selectRegistry,
|
||||
} from './prompt';
|
||||
@@ -57,6 +58,7 @@ export function ModalServiceAngular() {
|
||||
confirmChangePassword,
|
||||
confirmImageExport,
|
||||
confirmServiceForceUpdate,
|
||||
confirmStackUpdate,
|
||||
selectRegistry,
|
||||
confirmContainerDeletion,
|
||||
confirmKubeconfigSelection,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sanitize from 'sanitize-html';
|
||||
import bootbox from 'bootbox';
|
||||
import '@/portainer/components/BoxSelector/BoxSelectorItem.css';
|
||||
|
||||
import { applyBoxCSS, ButtonsOptions, confirmButtons } from './utils';
|
||||
|
||||
@@ -136,6 +137,46 @@ export function confirmServiceForceUpdate(
|
||||
customizeCheckboxPrompt(box, sanitizedMessage);
|
||||
}
|
||||
|
||||
export function confirmStackUpdate(
|
||||
message: string,
|
||||
defaultDisabled: boolean,
|
||||
defaultToggle: boolean,
|
||||
confirmButtonClassName: string | undefined,
|
||||
callback: PromptCallback
|
||||
) {
|
||||
const box = prompt({
|
||||
title: 'Are you sure?',
|
||||
inputType: 'checkbox',
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Pull latest image version<i></i>',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Update',
|
||||
className: confirmButtonClassName || 'btn-primary',
|
||||
},
|
||||
},
|
||||
callback,
|
||||
});
|
||||
box.find('.bootbox-body').prepend(message);
|
||||
const checkbox = box.find('.bootbox-input-checkbox');
|
||||
checkbox.prop('checked', defaultToggle);
|
||||
checkbox.prop('disabled', defaultDisabled);
|
||||
const checkboxDiv = box.find('.checkbox');
|
||||
checkboxDiv.removeClass('checkbox');
|
||||
checkboxDiv.prop(
|
||||
'style',
|
||||
'position: relative; display: block; margin-top: 10px; margin-bottom: 10px;'
|
||||
);
|
||||
const checkboxLabel = box.find('.form-check-label');
|
||||
checkboxLabel.addClass('switch box-selector-item limited business');
|
||||
const switchEle = checkboxLabel.find('i');
|
||||
switchEle.prop('style', 'margin-left:20px');
|
||||
}
|
||||
|
||||
export function confirmKubeconfigSelection(
|
||||
options: InputOption[],
|
||||
expiryMessage: string,
|
||||
|
||||
@@ -54,29 +54,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<por-switch-field
|
||||
label="'Allow self-signed certs'"
|
||||
checked="state.allowSelfSignedCerts"
|
||||
tooltip="'When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS'"
|
||||
on-change="(onToggleAllowSelfSignedCerts)"
|
||||
></por-switch-field>
|
||||
<div class="form-group">
|
||||
<por-switch-field
|
||||
label="'Allow self-signed certs'"
|
||||
checked="state.allowSelfSignedCerts"
|
||||
tooltip="'When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS'"
|
||||
on-change="(onToggleAllowSelfSignedCerts)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!isKubernetesDeploymentTabSelected()" style="margin-bottom: 60px">
|
||||
<label for="env_vars" class="col-sm-3 col-lg-2 control-label text-left" style="padding-left: 0; padding-top: 5px">
|
||||
Environment variables
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="env_vars" ng-model="formValues.EnvVarSource" placeholder="foo=bar,myvar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="'kubernetes'" heading="Kubernetes" ng-if="state.platformType === 'linux'">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
||||
dockerCommands[state.deploymentTab][state.platformType](agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
||||
}}</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="'swarm'" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
||||
dockerCommands[state.deploymentTab][state.platformType](agentVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
||||
}}</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="'standalone'" heading="Docker Standalone">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](agentVersion, agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
||||
dockerCommands[state.deploymentTab][state.platformType](agentVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
||||
}}</code>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px">
|
||||
|
||||
@@ -90,13 +90,22 @@ function EndpointController(
|
||||
|
||||
$scope.formValues = {
|
||||
SecurityFormData: new EndpointSecurityFormData(),
|
||||
EnvVarSource: '',
|
||||
};
|
||||
|
||||
$scope.isKubernetesDeploymentTabSelected = function () {
|
||||
return $scope.state.deploymentTab === DEPLOYMENT_TABS.KUBERNETES;
|
||||
};
|
||||
|
||||
$scope.copyEdgeAgentDeploymentCommand = copyEdgeAgentDeploymentCommand;
|
||||
function copyEdgeAgentDeploymentCommand() {
|
||||
let agentVersion = $scope.agentVersion;
|
||||
if ($scope.state.deploymentTab == DEPLOYMENT_TABS.KUBERNETES) {
|
||||
agentVersion = $scope.agentShortVersion;
|
||||
}
|
||||
|
||||
const command = $scope.dockerCommands[$scope.state.deploymentTab][$scope.state.platformType](
|
||||
$scope.agentVersion,
|
||||
$scope.agentShortVersion,
|
||||
agentVersion,
|
||||
$scope.endpoint.EdgeID,
|
||||
$scope.endpoint.EdgeKey,
|
||||
$scope.state.allowSelfSignedCerts
|
||||
@@ -314,89 +323,120 @@ function EndpointController(
|
||||
$scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
|
||||
}
|
||||
|
||||
function buildLinuxStandaloneCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent:${agentVersion}`;
|
||||
function buildEnvironmentSubCommand() {
|
||||
if ($scope.formValues.EnvVarSource === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $scope.formValues.EnvVarSource.split(',')
|
||||
.map(function (s) {
|
||||
if (s !== '') {
|
||||
return `-e ${s} \\`;
|
||||
}
|
||||
})
|
||||
.filter((s) => s !== undefined);
|
||||
}
|
||||
|
||||
function buildWindowsStandaloneCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent:${agentVersion}`;
|
||||
function buildLinuxStandaloneCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
const env = buildEnvironmentSubCommand();
|
||||
|
||||
return [
|
||||
'docker run -d \\',
|
||||
'-v /var/run/docker.sock:/var/run/docker.sock \\',
|
||||
'-v /var/lib/docker/volumes:/var/lib/docker/volumes \\',
|
||||
'-v /:/host \\',
|
||||
'-v portainer_agent_data:/data \\',
|
||||
'--restart always \\',
|
||||
'-e EDGE=1 \\',
|
||||
`-e EDGE_ID=${edgeId} \\`,
|
||||
`-e EDGE_KEY=${edgeKey} \\`,
|
||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
||||
...env,
|
||||
'--name portainer_edge_agent \\',
|
||||
`portainer/agent:${agentVersion}`,
|
||||
].join('\r\n');
|
||||
}
|
||||
|
||||
function buildLinuxSwarmCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_agent_network;
|
||||
function buildWindowsStandaloneCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
const env = buildEnvironmentSubCommand();
|
||||
|
||||
docker service create \\
|
||||
--name portainer_edge_agent \\
|
||||
--network portainer_agent_network \\
|
||||
-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--mode global \\
|
||||
--constraint 'node.platform.os == linux' \\
|
||||
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
|
||||
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\
|
||||
--mount type=bind,src=//,dst=/host \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=/data \\
|
||||
portainer/agent:${agentVersion}`;
|
||||
return [
|
||||
'docker run -d \\',
|
||||
'--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\',
|
||||
'--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\',
|
||||
'--mount type=volume,src=portainer_agent_data,dst=C:\\data \\',
|
||||
'--restart always \\',
|
||||
'-e EDGE=1 \\',
|
||||
`-e EDGE_ID=${edgeId} \\`,
|
||||
`-e EDGE_KEY=${edgeKey} \\`,
|
||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
||||
...env,
|
||||
'--name portainer_edge_agent \\',
|
||||
`portainer/agent:${agentVersion}`,
|
||||
].join('\r\n');
|
||||
}
|
||||
|
||||
function buildWindowsSwarmCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_edge_agent_network && \\
|
||||
docker service create \\
|
||||
--name portainer_edge_agent \\
|
||||
--network portainer_edge_agent_network \\
|
||||
-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--mode global \\
|
||||
--constraint node.platform.os==windows \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
portainer/agent:${agentVersion}`;
|
||||
function buildLinuxSwarmCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
const env = buildEnvironmentSubCommand();
|
||||
|
||||
return [
|
||||
'docker network create \\',
|
||||
'--driver overlay \\',
|
||||
'portainer_agent_network;',
|
||||
'',
|
||||
|
||||
'docker service create \\',
|
||||
'--name portainer_edge_agent \\',
|
||||
'--network portainer_agent_network \\',
|
||||
'-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\',
|
||||
'-e EDGE=1 \\',
|
||||
`-e EDGE_ID=${edgeId} \\`,
|
||||
`-e EDGE_KEY=${edgeKey} \\`,
|
||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
||||
...env,
|
||||
'--mode global \\',
|
||||
"--constraint 'node.platform.os == linux' \\",
|
||||
'--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\',
|
||||
'--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\',
|
||||
'--mount type=bind,src=//,dst=/host \\',
|
||||
'--mount type=volume,src=portainer_agent_data,dst=/data \\',
|
||||
`portainer/agent:${agentVersion}`,
|
||||
].join('\r\n');
|
||||
}
|
||||
|
||||
function buildKubernetesCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
curl https://downloads.portainer.io/portainer-ce${agentShortVersion}-edge-agent-setup.sh | bash -s -- ${edgeId} ${edgeKey} ${allowSelfSignedCerts ? '1' : ''}
|
||||
`;
|
||||
function buildWindowsSwarmCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
const env = buildEnvironmentSubCommand();
|
||||
|
||||
return [
|
||||
'docker network create \\',
|
||||
'--driver overlay \\',
|
||||
'portainer_agent_network;',
|
||||
'',
|
||||
|
||||
'docker service create \\',
|
||||
'--name portainer_edge_agent \\',
|
||||
'--network portainer_agent_network \\',
|
||||
'-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\',
|
||||
'-e EDGE=1 \\',
|
||||
`-e EDGE_ID=${edgeId} \\`,
|
||||
`-e EDGE_KEY=${edgeKey} \\`,
|
||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
||||
...env,
|
||||
'--mode global \\',
|
||||
"--constraint 'node.platform.os == windows' \\",
|
||||
'--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\',
|
||||
'--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\',
|
||||
'--mount type=volume,src=portainer_agent_data,dst=C:\\data \\',
|
||||
`portainer/agent:${agentVersion}`,
|
||||
].join('\r\n');
|
||||
}
|
||||
|
||||
function buildKubernetesCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `curl https://downloads.portainer.io/portainer-ce${agentVersion}-edge-agent-setup.sh | bash -s -- ${edgeId} ${edgeKey} ${allowSelfSignedCerts ? '1' : '0'}`;
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.canvas-container .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect, createRef } from 'react';
|
||||
import { KVM } from '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';
|
||||
|
||||
import { react2angular } from '@/react-tools/react2angular';
|
||||
@@ -12,29 +11,17 @@ export interface KVMControlProps {
|
||||
}
|
||||
|
||||
export function KVMControl({ deviceId, server, token }: KVMControlProps) {
|
||||
const divRef = createRef<HTMLInputElement>();
|
||||
useEffect(() => {
|
||||
if (divRef.current) {
|
||||
const connectButton = divRef.current.querySelector('button');
|
||||
if (connectButton) {
|
||||
connectButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!deviceId || !server || !token) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div ref={divRef}>
|
||||
<KVM
|
||||
deviceId={deviceId}
|
||||
mpsServer={`https://${server}/mps/ws/relay`}
|
||||
authToken={token}
|
||||
mouseDebounceTime="200"
|
||||
canvasHeight="100%"
|
||||
canvasWidth="100%"
|
||||
/>
|
||||
</div>
|
||||
<KVM
|
||||
deviceId={deviceId}
|
||||
mpsServer={`https://${server}/mps/ws/relay`}
|
||||
authToken={token}
|
||||
mouseDebounceTime="200"
|
||||
canvasHeight="100%"
|
||||
canvasWidth="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<div class="panel-body">
|
||||
<!-- toggle -->
|
||||
<div style="padding-bottom: 12px">
|
||||
<a ng-click="togglePanel()">
|
||||
<a ng-click="togglePanel()" data-cy="init-installPortainerFromBackup">
|
||||
<i ng-class="{ true: 'glyphicon glyphicon-chevron-down', false: 'glyphicon glyphicon-chevron-right' }[state.showRestorePortainer]" aria-hidden="true"></i
|
||||
><span style="padding-left: 10px">Restore Portainer from backup</span>
|
||||
</a>
|
||||
@@ -131,7 +131,7 @@
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="restore_file" checked="checked" />
|
||||
<label for="restore_file" style="padding-bottom: 20px">
|
||||
<label for="restore_file" style="padding-bottom: 20px" data-cy="init-selectLocalFile">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Upload backup file
|
||||
@@ -168,6 +168,7 @@
|
||||
ngf-accept="'application/x-tar,application/x-gzip'"
|
||||
ng-model="formValues.BackupFile"
|
||||
auto-focus
|
||||
data-cy="init-selectBackupFileButton"
|
||||
>Select file</button
|
||||
>
|
||||
<span style="margin-left: 5px">
|
||||
@@ -187,7 +188,7 @@
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" />
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="init-backupPasswordInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !password-input -->
|
||||
@@ -210,6 +211,7 @@
|
||||
ng-disabled="!formValues.BackupFile || state.backupInProgress"
|
||||
ng-click="uploadBackup()"
|
||||
button-spinner="state.backupInProgress"
|
||||
data-cy="init-restorePortainerButton"
|
||||
>
|
||||
<span ng-hide="state.backupInProgress">Restore Portainer</span>
|
||||
<span ng-show="state.backupInProgress">Restoring Portainer...</span>
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12" ng-if="$ctrl.settings">
|
||||
<settings-edge-compute on-submit="($ctrl.onSubmitEdgeCompute)" settings="($ctrl.settings)"></settings-edge-compute>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12" ng-if="$ctrl.settings">
|
||||
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12" ng-if="$ctrl.settings">
|
||||
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="form-group">
|
||||
<label for="helmrepository_url" class="col-sm-1 control-label text-left"> URL </label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="settings.HelmRepositoryURL" id="helmrepository_url" placeholder="https://charts.bitnami.com/bitnami" required />
|
||||
<input type="text" class="form-control" ng-model="settings.HelmRepositoryURL" id="helmrepository_url" placeholder="https://charts.bitnami.com/bitnami" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,7 +310,7 @@
|
||||
<div class="form-group">
|
||||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch">
|
||||
<label class="switch" data-cy="settings-s3PasswordToggle">
|
||||
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
@@ -320,7 +320,7 @@
|
||||
<div class="form-group" ng-if="formValues.passwordProtectS3">
|
||||
<label for="password" class="col-sm-1 control-label text-left">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" ng-model="formValues.passwordS3" id="password_S3" name="password_S3" required />
|
||||
<input type="password" class="form-control" ng-model="formValues.passwordS3" id="password_S3" name="password_S3" required data-cy="settings-backups3pw" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="backupPortainerForm.password_S3.$invalid">
|
||||
@@ -369,7 +369,9 @@
|
||||
<div class="form-group">
|
||||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch"> <input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i> </label>
|
||||
<label class="switch" data-cy="settings-passwordProtectLocal">
|
||||
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Password protect -->
|
||||
@@ -378,7 +380,7 @@
|
||||
<div class="form-group" ng-if="formValues.passwordProtect">
|
||||
<label for="password" class="col-sm-1 control-label text-left">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required />
|
||||
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required data-cy="settings-backupLocalPassword" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="backupPortainerForm.password.$invalid">
|
||||
@@ -399,6 +401,7 @@
|
||||
ng-click="downloadBackup()"
|
||||
ng-disabled="backupPortainerForm.$invalid || state.backupInProgress || state.featureLimited"
|
||||
button-spinner="state.backupInProgress"
|
||||
data-cy="settings-downloadLocalBackup"
|
||||
>
|
||||
<span ng-hide="state.backupInProgress">Download backup</span>
|
||||
<span ng-show="state.backupInProgress">Downloading backup</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuidv4 from 'uuid/v4';
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
|
||||
angular
|
||||
.module('portainer.app')
|
||||
@@ -31,8 +32,9 @@ angular
|
||||
) {
|
||||
$scope.onChangeTemplateId = onChangeTemplateId;
|
||||
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
|
||||
|
||||
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
|
||||
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -51,6 +53,7 @@ angular
|
||||
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||
RepositoryFetchInterval: '5m',
|
||||
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||
ShowForcePullImage: false,
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
@@ -310,7 +313,7 @@ angular
|
||||
}
|
||||
|
||||
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
|
||||
|
||||
$scope.formValues.ShowForcePullImage = $scope.state.StackType !== 3;
|
||||
try {
|
||||
const containers = await ContainerService.containers(true);
|
||||
$scope.containerNames = ContainerHelper.getContainerNames(containers);
|
||||
|
||||
@@ -157,6 +157,25 @@
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
|
||||
<div ng-if="state.Method !== 'repository' && isAdmin && applicationState.endpoint.type !== 4">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Create a Stack webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Create a webhook (or callback URI) to automate the update of this stack. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this stack."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch box-selector-item limited business" style="margin-left: 20px">
|
||||
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" /><i></i>
|
||||
</label>
|
||||
<be-feature-indicator feature="stackWebhookFeature"></be-feature-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- environment-variables -->
|
||||
<environment-variables-panel ng-model="formValues.Env" explanation="These values will be used as substitutions in the stack file" on-change="(handleEnvVarChange)">
|
||||
</environment-variables-panel>
|
||||
|
||||
@@ -160,6 +160,26 @@
|
||||
></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="isAdmin && applicationState.endpoint.type !== 4">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Create a Stack webhook
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Create a webhook (or callback URI) to automate the update of this stack. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this stack."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch box-selector-item limited business" style="margin-left: 20px">
|
||||
<input type="checkbox" ng-model="formValues.EnableWebhook" disabled="disabled" ng-checked="true" /><i></i>
|
||||
</label>
|
||||
<be-feature-indicator feature="stackWebhookFeature"></be-feature-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- environment-variables -->
|
||||
<div ng-if="stack">
|
||||
<environment-variables-panel
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
|
||||
angular.module('portainer.app').controller('StackController', [
|
||||
'$async',
|
||||
'$compile',
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
@@ -27,6 +29,7 @@ angular.module('portainer.app').controller('StackController', [
|
||||
'endpoint',
|
||||
function (
|
||||
$async,
|
||||
$compile,
|
||||
$q,
|
||||
$scope,
|
||||
$state,
|
||||
@@ -52,6 +55,9 @@ angular.module('portainer.app').controller('StackController', [
|
||||
endpoint
|
||||
) {
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
|
||||
$scope.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
migrationInProgress: false,
|
||||
@@ -216,32 +222,43 @@ angular.module('portainer.app').controller('StackController', [
|
||||
};
|
||||
|
||||
$scope.deployStack = function () {
|
||||
var stackFile = $scope.stackFileContent;
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
var prune = $scope.formValues.Prune;
|
||||
var stack = $scope.stack;
|
||||
const stack = $scope.stack;
|
||||
const tplCrop =
|
||||
'<div>Do you want to force an update of the stack?</div>' +
|
||||
'<div style="position: absolute; right: 110px; top: 48px; z-index: 999"><be-feature-indicator feature="stackPullImageFeature"></be-feature-indicator></div>';
|
||||
const template = angular.element(tplCrop);
|
||||
const html = $compile(template)($scope);
|
||||
// 'Do you want to force an update of the stack?'
|
||||
ModalService.confirmStackUpdate(html, true, true, null, function (result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
var stackFile = $scope.stackFileContent;
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||
var prune = $scope.formValues.Prune;
|
||||
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
// The EndpointID property is not available for these stacks, we can pass
|
||||
// the current endpoint identifier as a part of the update request. It will be used if
|
||||
// the EndpointID property is not defined on the stack.
|
||||
if (stack.EndpointId === 0) {
|
||||
stack.EndpointId = endpoint.Id;
|
||||
}
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
// The EndpointID property is not available for these stacks, we can pass
|
||||
// the current endpoint identifier as a part of the update request. It will be used if
|
||||
// the EndpointID property is not defined on the stack.
|
||||
if (stack.EndpointId === 0) {
|
||||
stack.EndpointId = endpoint.Id;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StackService.updateStack(stack, stackFile, env, prune)
|
||||
.then(function success() {
|
||||
Notifications.success('Stack successfully deployed');
|
||||
$scope.state.isEditorDirty = false;
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create stack');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
$scope.state.actionInProgress = true;
|
||||
StackService.updateStack(stack, stackFile, env, prune)
|
||||
.then(function success() {
|
||||
Notifications.success('Stack successfully deployed');
|
||||
$scope.state.isEditorDirty = false;
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create stack');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editorUpdate = function (cm) {
|
||||
|
||||
@@ -259,7 +259,7 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE;
|
||||
break;
|
||||
case 3:
|
||||
deployable = endpoint.mode.provider === DOCKER_STANDALONE;
|
||||
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE || endpoint.mode.provider === DOCKER_STANDALONE;
|
||||
break;
|
||||
}
|
||||
return deployable;
|
||||
|
||||
@@ -12597,9 +12597,9 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
lodash-es@^4.17.15, lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash-webpack-plugin@^0.11.6:
|
||||
|
||||
Reference in New Issue
Block a user