Compare commits

...

20 Commits

Author SHA1 Message Date
Prabhat Khera
733013e484 WIP: migrade data 2022-03-02 11:13:44 +13:00
Prabhat Khera
6c7b8f87a9 WIP: improvements 2022-03-01 09:43:03 +13:00
Prabhat Khera
690f6e8af3 WIP: migrating code 2022-02-25 14:01:48 +13:00
Prabhat Khera
abcf73a415 WIP: migration improvements 2022-02-24 13:55:32 +13:00
Chaim Lev-Ari
ff7847aaa5 chore(git): ignore prettier commits on git blame (#6584)
* chore(git): ignore prettier commits on git blame

* chore(vscode): fix launch command
2022-02-22 16:27:35 +02:00
Matt Hook
a89c3773dd fix(datastore): export/import the bolt sequence number EE-2451 (#6571)
* Implement setter/getter for the sequence

* import/export counts

* fix go tests.  rename vars

* Improved and simplified the logic. Made it more generic

* Remove unused methods

* remove unused methods

* not part of branch fix
2022-02-22 09:53:17 +13:00
Hao Zhang
5d75ca34ea fix(stack): git force pull image toggle only for non-kubernetes git based stacks (#6574) 2022-02-21 08:43:22 +08:00
Marcelo Rydel
d47a9d590e fix(kube): namespace parameter is not used in kube redeploy (#6569) 2022-02-18 16:36:20 +13:00
Anthony Lapenna
bd679ae806 feat(endpoint): add an input to source env vars [EE-2436] (#6517)
* feat(endpoint): add an input to source env vars

* fix(endpoint): fix invalid version in deployment instructions

* fix(endpoint): fix copy Edge command

* fix(endpoint): fix invalid Edge deployment instruction

* feat(endpoint): add missing parameter to edge deploy script

* feat(edge): use temporary manifest url

* refactor(endpoint): update method and placeholder

* fix(endpoint): fix missing agent name in Edge deployment instructions on Swarm

* fix(endpoint): fix invalid Edge deployment instructions for Kubernetes

* fix(build): commit yarn.lock

* chore(deps): run yarn

* feat(endpoint): do not support kubernetes with Edge env vars

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-02-17 10:25:59 +13:00
LP B
5de7ecb5f0 chore(deps): freeze lockfile on Github Actions (#6570) 2022-02-16 18:20:15 +01:00
Marcelo Rydel
b3cd9c69df fix(edge/settings): render view after loading settings [EE-2532] (#6560) 2022-02-15 18:26:42 -03:00
Chaim Lev-Ari
73311b6f32 fix(edge/devices): make actions button larger [EE-2471] (#6542)
* fix(edge/devices): make actions button larger [EE-2471]

fixes [EE-2471]

* fix(edge/devices): fix table-actions-title padding

Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-16 08:38:24 +13:00
Sven Dowideit
93ddcfecd9 fix(templates): show docker-compose app templates when in swarm mode [EE-2117] (#6177)
* fix(templates): EE-2117: show docker-compose app templates when in swarm mode and the user selects 'showContainers

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix(templates): keep original behavior for standalone

* fix(templates): display all templates on Swarm

* refactor(templates): update method name

Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2022-02-15 07:30:02 +13:00
Marcelo Rydel
2bffba7371 fix(edge): only show expand row for Edge Devices with AMT activated [EE-2489] (#6519) 2022-02-14 11:44:55 -03:00
Hao Zhang
37ca62eb06 feat(webhook): teasers of pull images and webhook for EE EE-1332 (#6278)
* feat(webhook): teasers of pull images and webhook for EE
2022-02-14 21:51:43 +08:00
Chaim Lev-Ari
fa208c7f2a docs(github): fix slack link [EE-2438] (#6541)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-11 10:07:14 -03:00
testA113
6fac3fa127 add data-cy attributes for backup/restore (#6546)
Co-authored-by: testA113 <42307911+aliharriss@users.noreply.github.com>
2022-02-11 15:24:44 +13:00
deviantony
171392c5ca chore(dev): update vscode example 2022-02-10 22:04:27 +00:00
Marcelo Rydel
d48ff2921b fix(edge): show KVM connect button, remove automatic useEffect [EE-2520] (#6540) 2022-02-10 14:23:09 -03:00
Chaim Lev-Ari
3165d354b5 fix(settings): clear helm url if requested [EE-2494] (#6526)
* fix(settings): clear helm url if requested [EE-2494]

fix [EE-2494]

before this PR, helm url would clear when updating settings, if the helm url key wasn't provided.
in this PR, it will be changed only if required

* fix(settings): allow empty helm repo

* chore(deps): run yarn

* fix(settings): set helm repo url
2022-02-10 06:03:46 +02:00
75 changed files with 1858 additions and 234 deletions

View File

@@ -1,4 +1,5 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169

View File

@@ -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

View File

@@ -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**

View File

@@ -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/
-->

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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"]
}

View File

@@ -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.

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -2,6 +2,7 @@ package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)

View 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
}

View 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
}

View 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: 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
}

View 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, &registry)
if err != nil {
return err
}
}
return nil
}
func v17_down_registries_to_18() error {
return nil
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View 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, &registry)
}
return nil
}
func v31_down_registries_to_32() error {
return nil
}

View 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
}

View File

@@ -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
}

View 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: 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
}

View 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
}

View 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

View 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
}

View File

@@ -0,0 +1,10 @@
package types
type Migration struct {
Version int
Up func() error
Down func() error
Completed bool
Timestamp int32
Name string
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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',

View File

@@ -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">

View File

@@ -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="...">

View File

@@ -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(),

View File

@@ -206,6 +206,7 @@ export function EdgeDevicesDatatable({
<RowProvider
key={key}
disableTrustOnFirstConnect={disableTrustOnFirstConnect}
isOpenAmtEnabled={isOpenAmtEnabled}
>
<TableRow<Environment>
cells={row.cells}

View File

@@ -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>;

View File

@@ -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 }}

View File

@@ -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] {

View File

@@ -1,3 +1,4 @@
.table-actions-title {
color: var(--blue-2);
padding: 5px 10px;
}

View File

@@ -0,0 +1,9 @@
.expand-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}

View File

@@ -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) {

View File

@@ -10,6 +10,7 @@ class GitFormAutoUpdateFieldsetController {
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
this.limitedFeature = FeatureId.FORCE_REDEPLOYMENT;
this.stackPullImageFeature = FeatureId.STACK_PULL_IMAGE;
}
copyWebhook() {

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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',
}

View File

@@ -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;

View 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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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">

View File

@@ -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();

View File

@@ -1,7 +1,3 @@
.canvas-container .header {
display: none;
}
.canvas-container {
display: contents;
}

View File

@@ -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%"
/>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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: