Compare commits

..

11 Commits

Author SHA1 Message Date
Sven Dowideit
b5133e3aa6 POC: support agent auto-upgrade
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-25 13:25:46 +10:00
Sven Dowideit
4f0e09fad8 start sending the actual configured stacks
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-21 14:23:52 +10:00
Sven Dowideit
428d2a03bf and using mtls certs
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-14 17:33:02 +10:00
Sven Dowideit
82a13560b3 and using mtls certs
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-14 17:32:42 +10:00
Sven Dowideit
6f354833b7 enough working so using curl can get portainer to record the env as having a heartbeat, and the timestamp is good.
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-14 17:22:39 +10:00
Sven Dowideit
ad5ece52b1 add a PORTAINER_DOCKER_FLAGS env to yarn so its easy to use different docker proxy options
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 13:11:35 +10:00
Sven Dowideit
7ddd02bf91 if --sslcacert is set, reject any Edge requests that don't have a ClientAuth cert signed by it
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 08:19:38 +10:00
Sven Dowideit
71c8932f76 fix things to add --sslcert, --sslkey, --sslcacert to agent
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 08:19:38 +10:00
Sven Dowideit
eab2ae6230 fix logging so we don't have 3 different output formats
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 08:19:38 +10:00
Sven Dowideit
133bd15dad need to separate the edge from non-edge api bouncer stats - edge pin swamps everything
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 08:19:38 +10:00
Sven Dowideit
b93a11b4e5 feat(dev-metrics) add a prometheus metrics endpoint
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2022-02-11 08:19:36 +10:00
91 changed files with 961 additions and 1868 deletions

View File

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

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 https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
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.
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 https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
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 Commercial setup.
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical 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 Commercial setup.
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical 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 https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->

View File

@@ -4,13 +4,14 @@ 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 https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
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 --frozen-lockfile
run: yarn
- 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 --frozen-lockfile
run: yarn
- name: Run tests
run: yarn test:client

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,

View File

@@ -4,5 +4,5 @@
"gopls": {
"build.expandWorkspaceToModule": false
},
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
"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 Portainer's community support channels [here.](https://www.portainer.io/community_help)
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
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

@@ -47,6 +47,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCacert: kingpin.Flag("sslcacert", "Path to the SSL CA certificate used to validate the edge agent cert").String(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),

View File

@@ -18,6 +18,7 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCacertPath = "/certs/portainer-ca.crt"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultBaseURL = "/"

View File

@@ -15,6 +15,7 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCacertPath = "C:\\certs\\portainer-ca.crt"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"

View File

@@ -1,9 +1,7 @@
package main
import (
"fmt"
"log"
"strings"
"github.com/sirupsen/logrus"
)
@@ -12,30 +10,14 @@ type portainerFormatter struct {
logrus.TextFormatter
}
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = 31 // gray
case logrus.WarnLevel:
levelColor = 33 // yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = 31 // red
default:
levelColor = 36 // blue
}
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
}
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatterLogrus)
logrus.SetFormatter(formatter)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)

View File

@@ -23,7 +23,6 @@ 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"
@@ -119,20 +118,13 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
logrus.Fatalf("Something Failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
m := migrations.NewMigrator(*store)
err = m.Migrate(storedVersion)
err = store.MigrateData()
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)
@@ -216,7 +208,7 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
func initSSLService(addr, dataPath, certPath, keyPath, cacertPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
@@ -225,7 +217,7 @@ func initSSLService(addr, dataPath, certPath, keyPath string, fileService portai
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
err := sslService.Init(host, certPath, keyPath)
err := sslService.Init(host, certPath, keyPath, cacertPath)
if err != nil {
return nil, err
}
@@ -576,7 +568,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, *flags.SSLCacert, fileService, dataStore, shutdownTrigger)
if err != nil {
logrus.Fatal(err)
}

View File

@@ -33,7 +33,4 @@ 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,43 +376,3 @@ 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,7 +2,6 @@ package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)

View File

@@ -1,53 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,46 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,58 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,82 +0,0 @@
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

@@ -1,39 +0,0 @@
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

@@ -1,94 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,68 +0,0 @@
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

@@ -1,57 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,109 +0,0 @@
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

@@ -1,117 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,39 +0,0 @@
#!/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

@@ -1,111 +0,0 @@
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

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

View File

@@ -33,7 +33,6 @@ 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"
)
@@ -69,8 +68,6 @@ 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 {
@@ -230,13 +227,6 @@ func (store *Store) initServices() error {
}
store.ScheduleService = scheduleService
authService := authorization.NewService(store)
if err != nil {
return err
}
store.AuthorizationService = authService
return nil
}
@@ -379,7 +369,6 @@ 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) {
@@ -572,11 +561,6 @@ 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
@@ -585,7 +569,6 @@ func (store *Store) Export(filename string) (err error) {
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
@@ -686,5 +669,5 @@ func (store *Store) Import(filename string) (err error) {
store.Webhook().UpdateWebhook(v.ID, &v)
}
return store.connection.RestoreMetadata(backup.Metadata)
return nil
}

View File

@@ -60,6 +60,8 @@ const (
DefaultSSLCertFilename = "cert.pem"
// DefaultSSLKeyFilename represents the default ssl key file name
DefaultSSLKeyFilename = "key.pem"
// DefaultSSLCacertFilename represents the default CA ssl certificate file name for mTLS
DefaultSSLCacertFilename = "ca-cert.pem"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
@@ -161,7 +163,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
}
if !exists {
return errors.New("File doesn't exist")
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
}
finput, err := os.Open(fromFilePath)
@@ -352,6 +354,9 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
// GetFileContent returns the content of a file as bytes.
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
if trustedRoot == "" {
trustedRoot = "/"
}
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
if err != nil {
if filePath == "" {
@@ -627,6 +632,25 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
return defCertPath, defKeyPath, nil
}
// GetDefaultSSLCacertsPath returns the ssl cacert path
func (service *Service) GetDefaultSSLCacertsPath() string {
cacertPath := JoinPaths(SSLCertPath, DefaultSSLCacertFilename)
return service.wrapFileStore(cacertPath)
}
// CopySSLCacert copies the specified cacert pem file
func (service *Service) CopySSLCacert(cacertPath string) (string, error) {
defCacertPath := service.GetDefaultSSLCacertsPath()
err := service.Copy(cacertPath, defCacertPath, true)
if err != nil {
return "", err
}
return defCacertPath, nil
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {

View File

@@ -19,10 +19,12 @@ require (
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v4.0.0+incompatible
github.com/google/gops v0.3.22
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-version v1.4.0
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
@@ -34,10 +36,12 @@ require (
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
github.com/prometheus/client_golang v1.7.1
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/swaggo/swag v1.7.8
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
@@ -50,6 +54,9 @@ require (
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
@@ -57,6 +64,8 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
@@ -70,6 +79,10 @@ require (
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.1.0 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
@@ -79,10 +92,13 @@ require (
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
@@ -93,6 +109,9 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.10.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
@@ -101,11 +120,12 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.7 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect

View File

@@ -41,6 +41,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@@ -62,9 +64,14 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -105,6 +112,7 @@ github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
@@ -120,7 +128,9 @@ github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8n
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
@@ -334,13 +344,23 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
@@ -404,6 +424,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gops v0.3.22 h1:lyvhDxfPLHAOR2xIYwjPhN387qHxyU21Sk9sz/GhmhQ=
github.com/google/gops v0.3.22/go.mod h1:7diIdLsqpCihPSX3fQagksT/Ku/y4RL9LHTlKyEUDl8=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -446,6 +468,8 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@@ -473,6 +497,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
@@ -493,6 +519,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -518,12 +545,15 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
@@ -603,6 +633,12 @@ github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -627,17 +663,20 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -649,6 +688,7 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777 h1:rDj3WeO+TiWyxfcydUnKegWAZoR5kQsnW0wzhggdOrw=
@@ -663,6 +703,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@@ -675,6 +716,7 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -705,10 +747,14 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
@@ -718,6 +764,7 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@@ -738,9 +785,11 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
@@ -804,6 +853,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -841,8 +892,10 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -922,10 +975,14 @@ golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
@@ -936,8 +993,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -982,6 +1040,8 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1085,6 +1145,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1144,6 +1205,7 @@ k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g=
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=

View File

@@ -0,0 +1,393 @@
package endpointedge
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"sync"
"time"
version "github.com/hashicorp/go-version"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/sirupsen/logrus"
)
// https://datatracker.ietf.org/doc/html/rfc6902
// Doing this manually, because at this point, i don't want to marshal to json, make a diff - for now, just using 'add' (as its really an upsert)
type JSONPatch struct {
Operation string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
// TODO: copied from edgestack_status_update
type updateStatusPayload struct {
Error string
Status *portainer.EdgeStackStatusType
EndpointID *portainer.EndpointID
}
type edgeJobResponse struct {
// EdgeJob Identifier
ID portainer.EdgeJobID `json:"Id" example:"2"`
// Whether to collect logs
CollectLogs bool `json:"CollectLogs" example:"true"`
// A cron expression to schedule this job
CronExpression string `json:"CronExpression" example:"* * * * *"`
// Script to run
Script string `json:"Script" example:"echo hello"`
// Version of this EdgeJob
Version int `json:"Version" example:"2"`
}
// An empty request ~~ just a ping.
type Snapshot struct {
Docker *portainer.DockerSnapshot
Kubernetes *portainer.KubernetesSnapshot
}
type AsyncRequest struct {
CommandId string `json: optional`
Snapshot *Snapshot `json: optional` // todo
StackStatus map[portainer.EdgeStackID]updateStatusPayload
}
func (payload *AsyncRequest) Validate(r *http.Request) error {
// TODO:
return nil
}
type AsyncResponse struct {
CommandInterval time.Duration `json: optional`
PingInterval time.Duration `json: optional`
SnapshotInterval time.Duration `json: optional`
ServerCommandId string // should be easy to detect if its larger / smaller: this is the response that tells the agent there are new commands waiting for it
SendDiffSnapshotTime time.Time `json: optional` // might be optional
Commands []JSONPatch `json: optional` // todo
Status string // give the agent some idea if the server thinks its OK, or if it should STOP
}
// Yup, this should be environment specific, and not global
var lastcheckinStatusMutex sync.Mutex
// for testing with mTLS..:
//sven@p1:~/src/portainer/portainer$ curl -k --cacert ~/.config/portainer/certs/ca.pem --cert ~/.config/portainer/certs/agent-cert.pem --key ~/.config/portainer/certs/agent-key.pem -X POST --header "X-PortainerAgent-EdgeID: 7e2b0143-c511-43c3-844c-a7a91ab0bedc" --data '{"CommandId": "okok", "Snapshot": {}}' https://p1:9443/api/endpoints/edge/async/
//{"CommandInterval":0,"PingInterval":0,"SnapshotInterval":0,"ServerCommandId":"8888","SendDiffSnapshotTime":"0001-01-01T00:00:00Z","Commands":{}}
// @id endpointAsync
// @summary Get environment(endpoint) status
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
// @description **Access policy**: restricted only to Edge environments(endpoints) TODO: with mTLS cert....
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} AsyncResponse "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access environment(endpoint)"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/edge/async/ [post]
func (handler *Handler) endpointAsync(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// TODO: get endpointID from the mTLS cert info
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).Debug("missing agent edge id")
return &httperror.HandlerError{http.StatusInternalServerError, "missing Edge identifier", errors.New("missing Edge identifier")}
}
// TODO: if the mTLS certs are valid, and we don't have a matching environment registered, CREATE IT (and maybe untrusted...)
endpoint, err := handler.getEdgeEndpoint(edgeIdentifier)
if err != nil {
// TODO: if its a valid cert, or the user hasn't limited to mTLS / portainer set id, the
// create new untrusted environment
// portainer.HTTPResponseAgentPlatform tells us what platform it is too...
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).WithField("Agent Addr", r.RemoteAddr).Debug("edge id not found in existing endpoints!")
}
// if agent mTLS is on, drop the connection if the client cert isn't CA'd (or if its revoked)
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
// TODO: an assumption that needs testing, is that using the right EdgeId means we're also talking to the same DOckerd / Kube cluster.
// I suspect that without getting the Dockerd UUID, or the "by convention" kube uuid, we can't know if someone's re-used info which then would cause some kind of state flapping.
// Don't ask me what happens in non-async mode if you have more than one agent running - surely that would make the tunnel go boom?
requestLogger := logrus.
WithField("Agent Addr", r.RemoteAddr).
WithField("Agent Version", r.Header.Get(portainer.HTTPAgentVersionHeaderName)).
WithField("Agent PID", r.Header.Get(portainer.HTTPAgentPIDName)).
WithField("Agent EdgeID", r.Header.Get(portainer.PortainerAgentEdgeIDHeader)) //.
//WithField("Agent UniqueID", r.Header.Get(portainer.HTTPAgentUUIDHeaderName))
// Any request we can identify as coming from a valid agent is treated as a Ping
endpoint.LastCheckInDate = time.Now().Unix()
endpoint.Status = portainer.EndpointStatusUp
// TODO: update endpoint contact time
lastcheckinStatusMutex.Lock()
if endpoint.AgentHistory == nil {
endpoint.AgentHistory = make(map[string]portainer.AgentInfo)
}
info, ok := endpoint.AgentHistory[r.RemoteAddr]
if !ok {
info = portainer.AgentInfo{
LastCheckInDate: endpoint.LastCheckInDate,
Version: r.Header.Get(portainer.HTTPAgentVersionHeaderName),
RemoteAddr: r.RemoteAddr,
Status: "OK",
}
}
info.CheckInCount = info.CheckInCount + 1
info.Status = "OK"
info.LastCheckInDate = endpoint.LastCheckInDate
requestLogger.Debugf("Checkin count = %d", info.CheckInCount)
endpoint.AgentHistory[r.RemoteAddr] = info
// Determine if there's more than one active agent, and if so, tell them to STOP
okCount := 0
infoVersion, _ := version.NewVersion(info.Version)
for key, val := range endpoint.AgentHistory {
timeToLastCheckIn := time.Second * time.Duration(endpoint.LastCheckInDate-val.LastCheckInDate)
// Timeout for last best agent (currently based on version number)
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*100) {
delete(endpoint.AgentHistory, key)
continue
}
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*10) {
val.Status = "GONE"
endpoint.AgentHistory[key] = val
continue
}
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*2) {
val.Status = "TROUBLED"
endpoint.AgentHistory[key] = val
continue
}
if val.Status == "OK" {
// if there's a info.Version difference, choose the more up to date agent...
valVersion, _ := version.NewVersion(val.Version)
if infoVersion.GreaterThan(valVersion) {
val.Status = "STOP VERSION - " + info.Version
endpoint.AgentHistory[val.RemoteAddr] = val
continue
}
if valVersion.GreaterThan(infoVersion) {
info.Status = "STOP VERSION - " + val.Version
endpoint.AgentHistory[info.RemoteAddr] = info
info = val
continue
}
// TODO: UX question - trade off between user expectation on upgrade, vs stability at staeday state.
// if we have two or more otherwise just as good agents, pick the newer one, on the presumption that it was started on purpose
// the risk with this is the flapping you get if you gave two identical agents with --restart always - so maybe it should get tuned
// for eg, only use the younger one if the user has initiated an upgrade? otherwise prefer the old?
if val.CheckInCount < info.CheckInCount {
info.Status = "STOP AGE"
endpoint.AgentHistory[info.RemoteAddr] = info
info = val
continue
}
okCount++
}
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
}
lastcheckinStatusMutex.Unlock()
var payload AsyncRequest
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
// an "" request ~~ same as {}
requestLogger.WithError(err).WithField("payload", r).Debug("decode payload")
}
//if endpoint.AgentHistory[r.RemoteAddr].Status != "OK" {
// okCount of zero can happen - basically, we havn't timed out a newer agent's last contact, and so we're hoping it will come back, so we refuse the old version
// we could instead allow this, cos in some circumstances, it may be better to have more than one agent giving the user control
requestLogger.Debugf("Checkin STATUS = %s (okCount = %d)", endpoint.AgentHistory[r.RemoteAddr].Status, okCount)
//}
asyncResponse := AsyncResponse{
ServerCommandId: "8888", // the most current id of a new command on the server
Status: endpoint.AgentHistory[r.RemoteAddr].Status,
}
// TODO: need a way to detect that these are changed, and send them to the agent...
// CommandInterval time.Duration `json: optional`
// PingInterval time.Duration `json: optional`
// SnapshotInterval time.Duration `json: optional`
if payload.CommandId == "" && payload.Snapshot == nil {
// just a ping.
return response.JSON(w, asyncResponse)
}
if payload.Snapshot != nil {
asyncResponse.SendDiffSnapshotTime = handler.saveSnapshot(requestLogger, endpoint, payload)
}
if payload.CommandId != "" {
asyncResponse.Commands = handler.sendCommandsSince(requestLogger, endpoint, payload.CommandId)
}
return response.JSON(w, asyncResponse)
}
// TODO: yup, next step is for these to be JSONDiff's and to be rehydrated
func (handler *Handler) saveSnapshot(requestLogger *logrus.Entry, endpoint *portainer.Endpoint, payload AsyncRequest) time.Time {
for stackID, status := range payload.StackStatus {
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
// TODO: work out what we can do with the errors
if err == nil {
stack.Status[*status.EndpointID] = portainer.EdgeStackStatus{
Type: *status.Status,
Error: status.Error,
EndpointID: *status.EndpointID,
}
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
}
}
switch endpoint.Type {
// case portainer.AzureEnvironment:
// return time.Now()
case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment:
requestLogger.Debug("Got a Kubernetes Snapshot")
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*payload.Snapshot.Kubernetes}
return time.Unix(payload.Snapshot.Kubernetes.Time, 0)
case portainer.DockerEnvironment, portainer.AgentOnDockerEnvironment, portainer.EdgeAgentOnDockerEnvironment:
requestLogger.Debug("Got a Docker Snapshot")
endpoint.Snapshots = []portainer.DockerSnapshot{*payload.Snapshot.Docker}
return time.Unix(payload.Snapshot.Docker.Time, 0)
default:
return time.Time{}
}
}
func (handler *Handler) sendCommandsSince(requestLogger *logrus.Entry, endpoint *portainer.Endpoint, commandId string) []JSONPatch {
var commandList []JSONPatch
// TODO: later, figure out if it is scalable to do diff's, as it means the server needs to store what it sent to all million agents (if the database had time based versioning, this would be trivial...)
// I suspect the easiest thing will be to add a "modified timestamp" to edge stacks and edge jobs, and to send them only when the modified time > requested time
requestLogger.WithField("endpoint", endpoint.Name).WithField("from command", commandId).Debug("Sending commands")
// schedules := []edgeJobResponse{}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
for _, job := range tunnel.Jobs {
schedule := edgeJobResponse{
ID: job.ID,
CronExpression: job.CronExpression,
CollectLogs: job.Endpoints[endpoint.ID].CollectLogs,
Version: job.Version,
}
file, err := handler.FileService.GetFileContent("/", job.ScriptPath)
if err != nil {
// TODO: this should maybe just skip thi job?
requestLogger.WithError(err).Error("Unable to retrieve Edge job script file")
continue
}
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
cmd := JSONPatch{
Operation: "add",
Path: fmt.Sprintf("/edgejob/%d", schedule.ID),
Value: schedule,
}
commandList = append(commandList, cmd)
}
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve relation object from the database")
return commandList
}
// TODO: this is the datatype the agent uses in the end
type edgeStackData struct {
ID portainer.EdgeStackID
Version int
StackFileContent string
Name string
}
for stackID := range relation.EdgeStacks {
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve edge stack from the database")
continue
}
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if handler.DataStore.IsErrObjectNotFound(err) {
requestLogger.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
continue
} else if err != nil {
requestLogger.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
continue
}
fileName := edgeStack.EntryPoint
if endpointutils.IsDockerEndpoint(endpoint) {
if fileName == "" {
requestLogger.Error("Docker is not supported by this stack")
continue
}
}
if endpointutils.IsKubernetesEndpoint(endpoint) {
fileName = edgeStack.ManifestPath
if fileName == "" {
requestLogger.Error("Kubernetes is not supported by this stack")
continue
}
}
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve Compose file from disk")
continue
}
stackStatus := edgeStackData{
StackFileContent: string(stackFileContent),
Name: edgeStack.Name,
ID: stack.ID,
Version: stack.Version,
}
cmd := JSONPatch{
Operation: "add",
Path: fmt.Sprintf("/edgestack/%d", stack.ID),
Value: stackStatus,
}
commandList = append(commandList, cmd)
}
return commandList
}
// TODO: this probably should be in the data layer.. (like, somewhere that depends dataservices/errors)
func (handler *Handler) getEdgeEndpoint(edgeIdentifier string) (*portainer.Endpoint, error) {
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return nil, err
}
for _, endpoint := range endpoints {
if endpoint.EdgeID == edgeIdentifier {
return &endpoint, nil
}
}
return nil, err
}

View File

@@ -31,5 +31,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
h.Handle("/{id}/edge/jobs/{jobID}/logs",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
h.Handle("/edge/async/",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointAsync))).Methods(http.MethodPost)
return h
}

View File

@@ -43,6 +43,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint)
// TODO: so huh? why are we getting the endpoint a second time? if there's a reason to do this - please add that as a comment!
latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
if latestEndpointReference == nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}

View File

@@ -11,6 +11,7 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
type stackStatusResponse struct {
@@ -48,6 +49,7 @@ type endpointStatusInspectResponse struct {
Stacks []stackStatusResponse `json:"stacks"`
}
// TODO: first up, why is this not in ../endpointedge/???
// @id EndpointStatusInspect
// @summary Get environment(endpoint) status
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
@@ -70,6 +72,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
logrus.WithError(err).WithField("env", endpointID).WithField("remote", r.RemoteAddr).Error("Unable to find an environment with the specified identifier inside the database")
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}

View File

@@ -21,6 +21,7 @@ import (
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/metrics"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -77,6 +78,7 @@ type Handler struct {
UserHandler *users.Handler
WebSocketHandler *websocket.Handler
WebhookHandler *webhooks.Handler
MetricsHandler *metrics.Handler
}
// @title PortainerCE API
@@ -241,6 +243,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/storybook"):
http.StripPrefix("/storybook", h.StorybookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/metrics"):
if h.MetricsHandler != nil {
h.MetricsHandler.ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/"):
h.FileHandler.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,33 @@
package metrics
import (
"github.com/google/gops/agent"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api/http/security"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
// Handler is the HTTP handler used to handle Prometheus metrics operations.
type Handler struct {
*mux.Router
}
// NewHandler creates a handler to manage settings operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/metrics", promhttp.Handler())
// h.Handle("/metrics", bouncer.PublicAccess(promhttp.Handler()))
logrus.Debugf("metricsHandler creation")
// also add gops agent support
if err := agent.Listen(agent.Options{}); err != nil {
logrus.WithError(err).Debugf("failed to start gops agent")
} else {
logrus.Debug("started gops agent")
}
return h
}

View File

@@ -211,6 +211,9 @@ 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

@@ -1,8 +1,10 @@
package security
import (
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
@@ -12,6 +14,9 @@ import (
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/sirupsen/logrus"
)
type (
@@ -37,6 +42,21 @@ type (
const apiKeyHeader = "X-API-KEY"
var (
apiProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "portainer_api_bouncer_results",
Help: "The total number of request access success/failures",
},
[]string{"permission", "path"},
)
agentApiProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "portainer_agent_api_bouncer_results",
Help: "The total number of agent request access success/failures",
},
[]string{"permission", "path"},
)
)
// NewRequestBouncer initializes a new RequestBouncer
func NewRequestBouncer(dataStore dataservices.DataStore, jwtService dataservices.JWTService, apiKeyService apikey.APIKeyService) *RequestBouncer {
return &RequestBouncer{
@@ -96,58 +116,111 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
tokenData, err := RetrieveTokenData(r)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
if tokenData.Role == portainer.AdministratorRole {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "admin"}).Inc()
return nil
}
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
group, err := bouncer.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
if !authorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
apiProcessed.With(prometheus.Labels{"permission": "denied"}).Inc()
return httperrors.ErrEndpointAccessDenied
}
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "ok"}).Inc()
return nil
}
// AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge environment(endpoint)
func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
// tls.RequireAndVerifyClientCert would be nice, but that would require the same certs for browser and api use
sslsettings, _ := bouncer.dataStore.SSLSettings().Settings()
if sslsettings.CacertPath != "" {
// if a caCert is set, then reject any requests that don't have a client Auth cert signed with it
if len(r.TLS.PeerCertificates) == 0 {
logrus.Error("No clientAuth Agent certificate offered")
return errors.New("No clientAuth Agent certificate offered")
}
caChainIdx := len(r.TLS.VerifiedChains)
chainCaCert := r.TLS.VerifiedChains[0][caChainIdx]
caCert, _ := ioutil.ReadFile(sslsettings.CacertPath)
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
logrus.
WithField("chain Subject", chainCaCert.Subject.String()).
WithField("tls DNSNames", chainCaCert.DNSNames).
WithField("Agent Addr", r.RemoteAddr).
WithField("Agent Version", r.Header.Get(portainer.HTTPAgentVersionHeaderName)).
WithField("Agent PID", r.Header.Get(portainer.HTTPAgentPIDName)).
WithField("Agent EdgeID", r.Header.Get(portainer.PortainerAgentEdgeIDHeader)).
Debugf("TLS client chain")
opts := x509.VerifyOptions{
//DNSName: name, // Not normally used on server side - important on the client side
Roots: certPool, // as used in ListenAndServe
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
remoteCert := r.TLS.PeerCertificates[0]
if _, err := remoteCert.Verify(opts); err != nil {
logrus.WithError(err).Error("Agent certificate not signed by the CACert")
return errors.New("Agent certificate wasn't signed by required CA Cert")
}
// TODO: test revoke cert list.
}
if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_error"}).Inc()
return errors.New("Invalid environment type")
}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_noiderror"}).Inc()
return errors.New("missing Edge identifier")
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_iderror"}).Inc()
return errors.New("invalid Edge identifier")
}
if endpoint.LastCheckInDate > 0 || endpoint.UserTrusted {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_ok"}).Inc()
return nil
}
settings, err := bouncer.dataStore.Settings().Settings()
if err != nil {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_error"}).Inc()
return fmt.Errorf("could not retrieve the settings: %w", err)
}
if settings.DisableTrustOnFirstConnect {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_untrusted"}).Inc()
return errors.New("the device has not been trusted yet")
}
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_ok"}).Inc()
return nil
}

View File

@@ -3,6 +3,7 @@ package http
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
@@ -34,6 +35,7 @@ import (
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/metrics"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -61,6 +63,7 @@ import (
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/scheduler"
stackdeployer "github.com/portainer/portainer/api/stacks"
"github.com/sirupsen/logrus"
)
// Server implements the portainer.Server interface
@@ -263,6 +266,11 @@ func (server *Server) Start() error {
webhookHandler.DataStore = server.DataStore
webhookHandler.DockerClientFactory = server.DockerClientFactory
var metricsHandler *metrics.Handler
if server.DataStore.Settings().IsFeatureFlagEnabled("dev-metrics") {
metricsHandler = metrics.NewHandler(requestBouncer)
}
server.Handler = &handler.Handler{
RoleHandler: roleHandler,
AuthHandler: authHandler,
@@ -299,6 +307,7 @@ func (server *Server) Start() error {
UserHandler: userHandler,
WebSocketHandler: websocketHandler,
WebhookHandler: webhookHandler,
MetricsHandler: metricsHandler,
}
handler := offlineGate.WaitingMiddleware(time.Minute, server.Handler)
@@ -330,6 +339,17 @@ func (server *Server) Start() error {
return server.SSLService.GetRawCertificate(), nil
}
if caCert := server.SSLService.GetCacertificatePem(); len(caCert) > 0 {
logrus.Debugf("using CA certificate for %s", server.BindAddressHTTPS)
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
httpsServer.TLSConfig.ClientCAs = certPool
// can't use tls.RequireAndVerifyClientCert, and this port is also used for the browser (though it would be a strong feature to allow the user to enable)
httpsServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
httpsServer.TLSConfig.BuildNameToCertificate()
}
go shutdown(server.ShutdownCtx, httpsServer)
return httpsServer.ListenAndServeTLS("", "")
}

View File

@@ -3,6 +3,7 @@ package ssl
import (
"context"
"crypto/tls"
"io/ioutil"
"log"
"os"
"time"
@@ -31,7 +32,7 @@ func NewService(fileService portainer.FileService, dataStore dataservices.DataSt
}
// Init initializes the service
func (service *Service) Init(host, certPath, keyPath string) error {
func (service *Service) Init(host, certPath, keyPath, cacertPath string) error {
pathSupplied := certPath != "" && keyPath != ""
if pathSupplied {
newCertPath, newKeyPath, err := service.fileService.CopySSLCertPair(certPath, keyPath)
@@ -39,7 +40,19 @@ func (service *Service) Init(host, certPath, keyPath string) error {
return errors.Wrap(err, "failed copying supplied certs")
}
return service.cacheInfo(newCertPath, newKeyPath, false)
newCacertPath := ""
if cacertPath != "" {
newCacertPath, err = service.fileService.CopySSLCacert(cacertPath)
if err != nil {
return errors.Wrap(err, "failed copying supplied cacert")
}
}
return service.cacheInfo(newCertPath, newKeyPath, newCacertPath, false)
}
if cacertPath != "" {
return errors.Errorf("supplying a CA cert path (%s) requires an SSL cert and key file", cacertPath)
}
settings, err := service.GetSSLSettings()
@@ -68,10 +81,24 @@ func (service *Service) Init(host, certPath, keyPath string) error {
return errors.Wrap(err, "failed generating self signed certs")
}
return service.cacheInfo(certPath, keyPath, true)
return service.cacheInfo(certPath, keyPath, "", true)
}
// GetRawCertificate gets the raw certificate
func (service *Service) GetCacertificatePem() (pemData []byte) {
settings, _ := service.GetSSLSettings()
if settings.CacertPath == "" {
return pemData
}
caCert, err := ioutil.ReadFile(settings.CacertPath)
if err != nil {
log.Printf("reading ca cert: %s", err)
return pemData
}
return caCert
}
// GetRawCertificate gets the raw certificate
func (service *Service) GetRawCertificate() *tls.Certificate {
return service.rawCert
@@ -98,7 +125,13 @@ func (service *Service) SetCertificates(certData, keyData []byte) error {
return err
}
service.cacheInfo(certPath, keyPath, false)
settings, err := service.dataStore.SSLSettings().Settings()
if err != nil {
return err
}
// Don't unset the settings.CacertPath when uploading a new cert from the UI
// TODO: should also add UI to update thecacert, or to disable it..
service.cacheInfo(certPath, keyPath, settings.CacertPath, false)
service.shutdownTrigger()
@@ -127,6 +160,7 @@ func (service *Service) SetHTTPEnabled(httpEnabled bool) error {
return nil
}
//TODO: why is this being cached in memory? is it actually loaded more than once?
func (service *Service) cacheCertificate(certPath, keyPath string) error {
rawCert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
@@ -138,7 +172,7 @@ func (service *Service) cacheCertificate(certPath, keyPath string) error {
return nil
}
func (service *Service) cacheInfo(certPath, keyPath string, selfSigned bool) error {
func (service *Service) cacheInfo(certPath, keyPath, cacertPath string, selfSigned bool) error {
err := service.cacheCertificate(certPath, keyPath)
if err != nil {
return err
@@ -151,6 +185,7 @@ func (service *Service) cacheInfo(certPath, keyPath string, selfSigned bool) err
settings.CertPath = certPath
settings.KeyPath = keyPath
settings.CacertPath = cacertPath
settings.SelfSigned = selfSigned
err = service.dataStore.SSLSettings().UpdateSettings(settings)

View File

@@ -116,6 +116,7 @@ type (
HTTPDisabled *bool
HTTPEnabled *bool
SSL *bool
SSLCacert *string
SSLCert *string
SSLKey *string
Rollback *bool
@@ -343,6 +344,17 @@ type (
// Deprecated in DBVersion == 22
Tags []string `json:"Tags"`
AgentHistory map[string]AgentInfo
}
AgentInfo struct {
LastCheckInDate int64
CheckInCount int64
Version string
RemoteAddr string
Status string
//UniqueId string
}
// EndpointAuthorizations represents the authorizations associated to a set of environments(endpoints)
@@ -837,6 +849,7 @@ type (
SSLSettings struct {
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
CacertPath string `json:"cacertPath"`
SelfSigned bool `json:"selfSigned"`
HTTPEnabled bool `json:"httpEnabled"`
}
@@ -1236,6 +1249,7 @@ type (
GetDefaultSSLCertsPath() (string, string)
StoreSSLCertPair(cert, key []byte) (string, string, error)
CopySSLCertPair(certPath, keyPath string) (string, string, error)
CopySSLCacert(cacertPath string) (string, error)
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
}
@@ -1363,6 +1377,10 @@ const (
PortainerAgentKubernetesSATokenHeader = "X-PortainerAgent-SA-Token"
// PortainerAgentSignatureMessage represents the message used to create a digital signature
// to be used when communicating with an agent
HTTPAgentVersionHeaderName = "X-Portainer-Agent-Version"
HTTPAgentPIDName = "X-Portainer-Process-Id"
HTTPAgentUUIDHeaderName = "X-Portainer-Agent-UUID"
PortainerAgentSignatureMessage = "Portainer-App"
// DefaultSnapshotInterval represents the default interval between each environment snapshot job
DefaultSnapshotInterval = "5m"
@@ -1383,7 +1401,7 @@ const (
)
// List of supported features
var SupportedFeatureFlags = []Feature{}
var SupportedFeatureFlags = []Feature{"dev-metrics"}
const (
_ AuthenticationMethod = iota

View File

@@ -3,7 +3,6 @@ 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';
@@ -66,7 +65,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,28 +65,6 @@
</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,19 +110,6 @@
<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,7 +2,6 @@ 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',
@@ -50,7 +49,6 @@ 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,7 +206,6 @@ export function EdgeDevicesDatatable({
<RowProvider
key={key}
disableTrustOnFirstConnect={disableTrustOnFirstConnect}
isOpenAmtEnabled={isOpenAmtEnabled}
>
<TableRow<Environment>
cells={row.cells}

View File

@@ -2,24 +2,21 @@ 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, isOpenAmtEnabled }),
[disableTrustOnFirstConnect, isOpenAmtEnabled]
() => ({ disableTrustOnFirstConnect }),
[disableTrustOnFirstConnect]
);
return <RowContext.Provider value={state}>{children}</RowContext.Provider>;

View File

@@ -1,9 +1,8 @@
import { CellProps, Column } from 'react-table';
import { CellProps, Column, TableInstance } 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',
@@ -16,15 +15,9 @@ export const name: Column<Environment> = {
sortType: 'string',
};
export function NameCell({ value: name, row }: CellProps<Environment>) {
const { isOpenAmtEnabled } = useRowContext();
const showExpandedRow = !!(
isOpenAmtEnabled &&
row.original.AMTDeviceGUID &&
row.original.AMTDeviceGUID.length > 0
);
export function NameCell({ value: name, row }: CellProps<TableInstance>) {
return (
<ExpandingCell row={row} showExpandArrow={showExpandedRow}>
<ExpandingCell row={row}>
<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 15px;
padding: 5px 5px !important;
}
.table-actions-menu-btn {
border: none;
background: none;
padding: 0 10px;
}
[data-reach-menu-link] {

View File

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

View File

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

View File

@@ -1,30 +1,20 @@
import { PropsWithChildren } from 'react';
import { Row } from 'react-table';
import { Row, TableInstance } from 'react-table';
import styles from './ExpandingCell.module.css';
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
row: Row<D>;
showExpandArrow: boolean;
interface Props {
row: Row<TableInstance>;
}
export function ExpandingCell<
D extends Record<string, unknown> = Record<string, unknown>
>({ row, showExpandArrow, children }: PropsWithChildren<Props<D>>) {
export function ExpandingCell({ children, row }: PropsWithChildren<Props>) {
return (
<>
{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>
)}
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...row.getToggleRowExpandedProps()}>
<i
className={`fas ${arrowClass(row.isExpanded)} space-right`}
aria-hidden="true"
/>
{children}
</>
</div>
);
function arrowClass(isExpanded: boolean) {

View File

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

View File

@@ -51,11 +51,6 @@
/>
</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,19 +1,16 @@
import uuidv4 from 'uuid/v4';
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { FeatureId } from 'Portainer/feature-flags/enums';
class StackRedeployGitFormController {
/* @ngInject */
constructor($async, $state, $compile, $scope, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
constructor($async, $state, 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,
@@ -34,7 +31,6 @@ class StackRedeployGitFormController {
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
RepositoryFetchInterval: '5m',
RepositoryWebhookURL: '',
ShowForcePullImage: false,
},
};
@@ -90,21 +86,27 @@ class StackRedeployGitFormController {
}
async submit() {
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;
}
return this.$async(async () => {
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;
this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues);
await 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');
this.$state.reload();
await this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying stack');
} finally {
@@ -152,7 +154,6 @@ 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,26 +16,12 @@ 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 (this.filterByTemplateType(template)) {
if ((template.Type === 1 && ctrl.state.showContainerTemplates) || (template.Type === 2 && ctrl.showSwarmStacks) || (template.Type === 3 && !ctrl.showSwarmStacks)) {
availableCategories = availableCategories.concat(template.Categories);
}
}
@@ -51,6 +37,13 @@ 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.filterByTemplateType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
model="template"
type-label="{{ template.Type === 1 ? 'container' : 'stack' }}"
on-select="($ctrl.selectAction)"
@@ -55,10 +55,7 @@
</template-item-actions>
</template-item>
<div ng-if="!$ctrl.templates" class="text-center text-muted"> Loading... </div>
<div
ng-if="($ctrl.templates | filter: $ctrl.filterByTemplateType | filter: $ctrl.filterByCategory | filter: $ctrl.state.textFilter).length === 0"
class="text-center text-muted"
>
<div ng-if="($ctrl.templates | filter: $ctrl.filterByType | filter: $ctrl.filterByCategory | filter: $ctrl.state.textFilter).length === 0" class="text-center text-muted">
No templates available.
</div>
</div>

View File

@@ -23,7 +23,4 @@ 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,9 +27,6 @@ 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

@@ -1,16 +0,0 @@
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,7 +22,6 @@ import {
confirmContainerDeletion,
confirmContainerRecreation,
confirmServiceForceUpdate,
confirmStackUpdate,
confirmKubeconfigSelection,
selectRegistry,
} from './prompt';
@@ -58,7 +57,6 @@ export function ModalServiceAngular() {
confirmChangePassword,
confirmImageExport,
confirmServiceForceUpdate,
confirmStackUpdate,
selectRegistry,
confirmContainerDeletion,
confirmKubeconfigSelection,

View File

@@ -1,6 +1,5 @@
import sanitize from 'sanitize-html';
import bootbox from 'bootbox';
import '@/portainer/components/BoxSelector/BoxSelectorItem.css';
import { applyBoxCSS, ButtonsOptions, confirmButtons } from './utils';
@@ -137,46 +136,6 @@ 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,44 +54,29 @@
</div>
</div>
<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>
<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 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](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, 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, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
}}</code>
<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>
</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, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
}}</code>
<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>
</uib-tab>
</uib-tabset>
<div style="margin-top: 10px">

View File

@@ -90,22 +90,13 @@ 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](
agentVersion,
$scope.agentVersion,
$scope.agentShortVersion,
$scope.endpoint.EdgeID,
$scope.endpoint.EdgeKey,
$scope.state.allowSelfSignedCerts
@@ -323,120 +314,89 @@ function EndpointController(
$scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
}
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 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 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 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 buildWindowsStandaloneCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
const env = buildEnvironmentSubCommand();
function buildLinuxSwarmCommand(agentVersion, agentShortVersion, edgeId, edgeKey, allowSelfSignedCerts) {
return `
docker network create \\
--driver overlay \\
portainer_agent_network;
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');
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}`;
}
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 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 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'}`;
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' : ''}
`;
}
initView();

View File

@@ -109,7 +109,7 @@
<div class="panel-body">
<!-- toggle -->
<div style="padding-bottom: 12px">
<a ng-click="togglePanel()" data-cy="init-installPortainerFromBackup">
<a ng-click="togglePanel()">
<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" data-cy="init-selectLocalFile">
<label for="restore_file" style="padding-bottom: 20px">
<div class="boxselector_header">
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px"></i>
Upload backup file
@@ -168,7 +168,6 @@
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">
@@ -188,7 +187,7 @@
></portainer-tooltip>
</label>
<div class="col-sm-4">
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="init-backupPasswordInput" />
<input type="password" class="form-control" ng-model="formValues.Password" id="password" />
</div>
</div>
<!-- !password-input -->
@@ -211,7 +210,6 @@
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" ng-if="$ctrl.settings">
<div class="col-sm-12">
<settings-edge-compute on-submit="($ctrl.onSubmitEdgeCompute)" settings="($ctrl.settings)"></settings-edge-compute>
</div>
</div>
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.settings">
<div class="col-sm-12">
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
</div>
</div>
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.settings">
<div class="col-sm-12">
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
</div>
</div>

View File

@@ -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" data-cy="settings-s3PasswordToggle">
<label class="switch">
<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 data-cy="settings-backups3pw" />
<input type="password" class="form-control" ng-model="formValues.passwordS3" id="password_S3" name="password_S3" required />
</div>
</div>
<div class="form-group col-md-12" ng-show="backupPortainerForm.password_S3.$invalid">
@@ -369,9 +369,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" data-cy="settings-passwordProtectLocal">
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i>
</label>
<label class="switch"> <input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i> </label>
</div>
</div>
<!-- !Password protect -->
@@ -380,7 +378,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 data-cy="settings-backupLocalPassword" />
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required />
</div>
</div>
<div class="form-group col-md-12" ng-show="backupPortainerForm.password.$invalid">
@@ -401,7 +399,6 @@
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,7 +4,6 @@ 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')
@@ -32,9 +31,8 @@ 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: '',
@@ -53,7 +51,6 @@ angular
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
RepositoryFetchInterval: '5m',
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
ShowForcePullImage: false,
};
$scope.state = {
@@ -313,7 +310,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,25 +157,6 @@
</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,26 +160,6 @@
></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,9 +1,7 @@
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',
@@ -29,7 +27,6 @@ angular.module('portainer.app').controller('StackController', [
'endpoint',
function (
$async,
$compile,
$q,
$scope,
$state,
@@ -55,9 +52,6 @@ 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,
@@ -222,43 +216,32 @@ angular.module('portainer.app').controller('StackController', [
};
$scope.deployStack = function () {
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;
var stackFile = $scope.stackFileContent;
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
var prune = $scope.formValues.Prune;
var stack = $scope.stack;
// 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_SWARM_MODE || endpoint.mode.provider === DOCKER_STANDALONE;
deployable = endpoint.mode.provider === DOCKER_STANDALONE;
break;
}
return deployable;

50
build/mtlscerts.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# very much just copied from https://lemariva.com/blog/2019/12/portainer-managing-docker-engine-remotely
# production use should involve a real external certificate management system
export HOST=portainer.p1.alho.st
export CERTDIR=~/.config/portainer/certs/
mkdir -p ${CERTDIR}
cd ${CERTDIR}
echo "Generating example mTLS certs into $(pwd)"
if [[ ! -f "ca.pem" ]]; then
echo "Generate the CA Cert"
openssl genrsa -aes256 -out ca-key.pem 4096
# enter a pass phrase to protect the ca-key
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
else
echo "ca.pem ca cert already exists"
fi
if [[ ! -f "server-cert.pem" ]]; then
echo "Generate the Portainer server cert"
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr
echo subjectAltName = DNS:$HOST,IP:10.0.0.200,IP:127.0.0.1,IP:10.10.10.189 >> extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile extfile.cnf
else
echo "server-cert.pem ca cert already exists"
fi
if [[ ! -f "agent-cert.pem" ]]; then
echo "Generate an Agent cert"
openssl genrsa -out agent-key.pem 4096
openssl req -subj '/CN=client' -new -key agent-key.pem -out agent-client.csr
echo extendedKeyUsage = clientAuth > agent-extfile.cnf
openssl x509 -req -days 365 -sha256 -in agent-client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out agent-cert.pem -extfile agent-extfile.cnf
else
echo "agent-cert.pem ca cert already exists"
fi
echo "done: Generated example mTLS certs into $(pwd)"

View File

@@ -159,6 +159,7 @@ function shell_run_container() {
const portainerData = '${PORTAINER_DATA:-/tmp/portainer}';
const portainerRoot = process.env.PORTAINER_PROJECT ? process.env.PORTAINER_PROJECT : process.env.PWD;
const portainerFlags = '${PORTAINER_FLAGS:-}';
const portainerDockerFlags = '${PORTAINER_DOCKER_FLAGS:-}';
return `
docker rm -f portainer
@@ -166,6 +167,7 @@ function shell_run_container() {
-p 8000:8000 \
-p 9000:9000 \
-p 9443:9443 \
${portainerDockerFlags} \
-v ${portainerRoot}/dist:/app \
-v ${portainerData}:/data \
-v /var/run/docker.sock:/var/run/docker.sock:z \

View File

@@ -12599,7 +12599,7 @@ locate-path@^6.0.0:
lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash-webpack-plugin@^0.11.6: