Compare commits

..

22 Commits

Author SHA1 Message Date
andres-portainer
da3856503f [WIP] Optimize buildEdgeStacks()
Some checks failed
Test / test-client (push) Has been cancelled
2022-05-11 19:38:55 -03:00
andres-portainer
0b6362a4bd Merge branch 'develop' into debug-api-endpoint 2022-05-10 18:00:25 -03:00
andres-portainer
8bbb6e5a6e Revert "Add caching to EdgeStack."
This reverts commit ea71ce44fa.
2022-05-08 16:41:11 -03:00
andres-portainer
ea71ce44fa Add caching to EdgeStack. 2022-05-06 21:45:06 -03:00
matias.spinarolli
a3f2b4b0af feat(ssl): use ECDSA instead of RSA to generate the self-signed certificates EE-3097 2022-05-06 19:35:52 -03:00
andres-portainer
f4c7896046 Add cache invalidation to avoid breaking data migration. 2022-05-05 19:40:40 -03:00
andres-portainer
b1272b9da3 Merge branch 'develop' into debug-api-endpoint 2022-05-05 19:17:57 -03:00
andres-portainer
943c0a6256 Avoid allocating on nil values. 2022-05-05 19:12:06 -03:00
andres-portainer
d245e196c1 Merge branch 'debug-api-endpoint' of github.com:portainer/portainer into debug-api-endpoint 2022-05-05 18:45:47 -03:00
andres-portainer
d6abf03d42 Cache the Settings object. 2022-05-05 18:44:11 -03:00
andres-portainer
f98585d832 Avoid a call to Endpoint().GetNextIdentifier(). 2022-05-05 18:43:16 -03:00
deviantony
bc3e973830 Merge branch 'develop' into debug-api-endpoint 2022-05-05 21:31:20 +00:00
deviantony
0292523855 refactor(edge): rollback to original error message 2022-05-04 18:30:05 +00:00
deviantony
044d756626 feat(edge): remove logrus calls 2022-05-04 18:28:31 +00:00
deviantony
a9c0c5f835 feat(edge): rollback go routine changes 2022-05-04 18:13:58 +00:00
deviantony
2b0a519c36 feat(edge): update edge status logic 2022-05-04 01:42:55 +00:00
yi-portainer
871da94da0 * remove endpoint update for testing 2022-05-04 12:07:27 +12:00
andres-portainer
81faf20f20 Add pprof handlers. 2022-05-03 19:42:26 -03:00
deviantony
b8757ac8eb Merge branch 'develop' into debug-api-endpoint 2022-05-03 04:34:27 +00:00
deviantony
b927e08d5e feat(http): add debug logs 2022-05-03 04:33:50 +00:00
deviantony
e2188edc9d feat(http): update logging format 2022-04-30 20:05:16 +00:00
deviantony
97d2a3bdf3 feat(http): add debug logs in 2022-04-30 19:45:46 +00:00
1749 changed files with 28336 additions and 41261 deletions

View File

@@ -31,12 +31,7 @@ rules:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
@@ -46,7 +41,6 @@ settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
@@ -58,7 +52,6 @@ overrides:
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
extends:
- airbnb
- airbnb-typescript
@@ -75,14 +68,7 @@ overrides:
version: 'detect'
rules:
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
@@ -104,7 +90,6 @@ overrides:
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
- files:
- app/**/*.test.*
extends:

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache

View File

@@ -3,7 +3,6 @@ import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
// Initialize MSW
initMSW({
@@ -32,17 +31,11 @@ export const parameters = {
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
export const decorators = [
(Story) => (
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
),
mswDecorator,
];

View File

@@ -22,7 +22,7 @@ Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
**The latest version of Portainer is 2.13.x**.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
## Getting started

View File

@@ -1,71 +0,0 @@
package agent
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil {
return 0, "", err
}
parsedURL.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return 0, "", err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
version := resp.Header.Get(portainer.PortainerAgentHeader)
if version == "" {
return 0, "", errors.New("Version Header is missing")
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, "", errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, "", err
}
if agentPlatformNumber == 0 {
return 0, "", errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), version, nil
}

View File

@@ -1,9 +0,0 @@
package build
// Variables to be set during the build time
var BuildNumber string
var ImageTag string
var NodejsVersion string
var YarnVersion string
var WebpackVersion string
var GoVersion string

View File

@@ -35,7 +35,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),

View File

@@ -16,7 +16,6 @@ import (
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
@@ -24,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/demo"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
@@ -574,7 +572,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
@@ -610,7 +607,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
reverseTunnelService.ProxyManager = proxyManager
@@ -637,14 +634,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
applicationStatus := initStatus(instanceID)
demoService := demo.NewService()
if *flags.DemoEnvironment {
err := demoService.Init(dataStore, cryptoService)
if err != nil {
log.Fatalf("failed initializing demo environment: %v", err)
}
}
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
logrus.Fatalf("Failed initializing environment: %v", err)
@@ -733,7 +722,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
}
}
@@ -744,15 +732,7 @@ func main() {
for {
server := buildServer(flags)
logrus.WithFields(logrus.Fields{
"Version": portainer.APIVersion,
"BuildNumber": build.BuildNumber,
"ImageTag": build.ImageTag,
"NodejsVersion": build.NodejsVersion,
"YarnVersion": build.YarnVersion,
"WebpackVersion": build.WebpackVersion,
"GoVersion": build.GoVersion},
).Print("[INFO] [cmd,main] Starting Portainer")
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
err := server.Start()
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
}

View File

@@ -10,7 +10,7 @@ import (
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)

View File

@@ -1,6 +1,7 @@
package endpoint
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
@@ -83,6 +84,15 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
return service.connection.CreateObjectWithSetSequence(BucketName, int(endpoint.ID), endpoint)
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service *Service) CreateWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
if endpoint.ID > 0 {
return errors.New("the endpoint must not have an ID")
}
return service.connection.CreateObject(BucketName, fn)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)

View File

@@ -97,6 +97,7 @@ type (
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
Endpoints() ([]portainer.Endpoint, error)
Create(endpoint *portainer.Endpoint) error
CreateWithCallback(endpoint *portainer.Endpoint, fn func(uint64) (int, interface{})) error
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
DeleteEndpoint(ID portainer.EndpointID) error
GetNextIdentifier() int

View File

@@ -1,6 +1,8 @@
package settings
import (
"sync"
portainer "github.com/portainer/portainer/api"
)
@@ -13,6 +15,30 @@ const (
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
cache *portainer.Settings
mu sync.RWMutex
}
func cloneSettings(src *portainer.Settings) *portainer.Settings {
if src == nil {
return nil
}
c := *src
if c.BlackListedLabels != nil {
c.BlackListedLabels = make([]portainer.Pair, len(src.BlackListedLabels))
copy(c.BlackListedLabels, src.BlackListedLabels)
}
if src.FeatureFlagSettings != nil {
c.FeatureFlagSettings = make(map[portainer.Feature]bool)
for k, v := range src.FeatureFlagSettings {
c.FeatureFlagSettings[k] = v
}
}
return &c
}
func (service *Service) BucketName() string {
@@ -33,6 +59,18 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
service.mu.RLock()
if service.cache != nil {
s := cloneSettings(service.cache)
service.mu.RUnlock()
return s, nil
}
service.mu.RUnlock()
service.mu.Lock()
defer service.mu.Unlock()
var settings portainer.Settings
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
@@ -40,12 +78,24 @@ func (service *Service) Settings() (*portainer.Settings, error) {
return nil, err
}
service.cache = cloneSettings(&settings)
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
service.mu.Lock()
defer service.mu.Unlock()
err := service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
if err != nil {
return err
}
service.cache = cloneSettings(settings)
return nil
}
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
@@ -61,3 +111,9 @@ func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
return false
}
func (service *Service) InvalidateCache() {
service.mu.Lock()
service.cache = nil
service.mu.Unlock()
}

View File

@@ -103,26 +103,8 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
if err := store.Close(); err != nil {
return options.BackupPath, fmt.Errorf(
"error closing datastore before creating backup: %v",
err,
)
}
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
return options.BackupPath, err
}
if _, err := store.Open(); err != nil {
return options.BackupPath, fmt.Errorf(
"error opening datastore after creating backup: %v",
err,
)
}
return options.BackupPath, nil
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
}
// RestoreWithOptions previously saved backup for the current Edition with options

View File

@@ -177,7 +177,7 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
}
store.EndpointRelation().Create(relation)

View File

@@ -47,9 +47,6 @@ func (store *Store) checkOrCreateDefaultSettings() error {
EnableTelemetry: true,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
InternalAuthSettings: portainer.InternalAuthSettings{
RequiredPasswordLength: 12,
},
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,

View File

@@ -31,6 +31,8 @@ func (store *Store) MigrateData() error {
return werrors.Wrap(err, "while backing up db before migration")
}
store.SettingsService.InvalidateCache()
migratorParams := &migrator.MigratorParameters{
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,

View File

@@ -34,9 +34,9 @@ func TestMigrateData(t *testing.T) {
wantPath string
}{
{
testName: "migrate version 24 to latest",
testName: "migrate version 24 to 35",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
wantPath: "test_data/output_35.json",
},
}
for _, test := range snapshotTests {

View File

@@ -100,12 +100,6 @@ func (m *Migrator) Migrate() error {
// Portainer 2.13
newMigration(40, m.migrateDBVersionToDB40),
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
}
var lastDbVersion int

View File

@@ -54,7 +54,7 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
}
err = m.endpointRelationService.Create(relation)

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log"
"github.com/docker/docker/api/types/volume"
"github.com/portainer/portainer/api/dataservices/errors"
portainer "github.com/portainer/portainer/api"
@@ -211,14 +210,14 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
continue
}
volumesData := snapshot.SnapshotRaw.Volumes
if volumesData.Volumes == nil {
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
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)
}
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
}
for _, resourceControl := range volumeResourceControls {
@@ -241,11 +240,18 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
return nil
}
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.VolumeListOKBody, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
volumes := volumesData.Volumes
for _, volume := range volumes {
volumeName := volume.Name
createTime := volume.CreatedAt
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]

View File

@@ -1,20 +0,0 @@
package migrator
import (
"github.com/pkg/errors"
)
func (m *Migrator) migrateDBVersionToDB50() error {
return m.migratePasswordLengthSettings()
}
func (m *Migrator) migratePasswordLengthSettings() error {
migrateLog.Info("Updating required password length")
s, err := m.settingsService.Settings()
if err != nil {
return errors.Wrap(err, "unable to retrieve settings")
}
s.InternalAuthSettings.RequiredPasswordLength = 12
return m.settingsService.UpdateSettings(s)
}

View File

@@ -1,30 +0,0 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB60() error {
if err := m.addGpuInputFieldDB60(); err != nil {
return err
}
return nil
}
func (m *Migrator) addGpuInputFieldDB60() error {
migrateLog.Info("- add gpu input field")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -27,9 +27,6 @@
],
"endpoints": [
{
"Agent": {
"Version": ""
},
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {
@@ -38,15 +35,8 @@
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"Gpus": [],
"GroupId": 1,
"Id": 1,
"IsEdgeDevice": false,
@@ -80,107 +70,12 @@
"DockerSnapshotRaw": {
"Containers": null,
"Images": null,
"Info": {
"Architecture": "",
"BridgeNfIp6tables": false,
"BridgeNfIptables": false,
"CPUSet": false,
"CPUShares": false,
"CgroupDriver": "",
"ContainerdCommit": {
"Expected": "",
"ID": ""
},
"Containers": 0,
"ContainersPaused": 0,
"ContainersRunning": 0,
"ContainersStopped": 0,
"CpuCfsPeriod": false,
"CpuCfsQuota": false,
"Debug": false,
"DefaultRuntime": "",
"DockerRootDir": "",
"Driver": "",
"DriverStatus": null,
"ExperimentalBuild": false,
"GenericResources": null,
"HttpProxy": "",
"HttpsProxy": "",
"ID": "",
"IPv4Forwarding": false,
"Images": 0,
"IndexServerAddress": "",
"InitBinary": "",
"InitCommit": {
"Expected": "",
"ID": ""
},
"Isolation": "",
"KernelMemory": false,
"KernelMemoryTCP": false,
"KernelVersion": "",
"Labels": null,
"LiveRestoreEnabled": false,
"LoggingDriver": "",
"MemTotal": 0,
"MemoryLimit": false,
"NCPU": 0,
"NEventsListener": 0,
"NFd": 0,
"NGoroutines": 0,
"Name": "",
"NoProxy": "",
"OSType": "",
"OSVersion": "",
"OomKillDisable": false,
"OperatingSystem": "",
"PidsLimit": false,
"Plugins": {
"Authorization": null,
"Log": null,
"Network": null,
"Volume": null
},
"RegistryConfig": null,
"RuncCommit": {
"Expected": "",
"ID": ""
},
"Runtimes": null,
"SecurityOptions": null,
"ServerVersion": "",
"SwapLimit": false,
"Swarm": {
"ControlAvailable": false,
"Error": "",
"LocalNodeState": "",
"NodeAddr": "",
"NodeID": "",
"RemoteManagers": null
},
"SystemTime": "",
"Warnings": null
},
"Info": null,
"Networks": null,
"Version": {
"ApiVersion": "",
"Arch": "",
"GitCommit": "",
"GoVersion": "",
"Os": "",
"Platform": {
"Name": ""
},
"Version": ""
},
"Volumes": {
"Volumes": null,
"Warnings": null
}
"Version": null,
"Volumes": null
},
"DockerVersion": "20.10.13",
"GpuUseAll": false,
"GpuUseList": null,
"HealthyContainerCount": 0,
"ImageCount": 9,
"NodeCount": 0,
@@ -694,12 +589,6 @@
"BlackListedLabels": [],
"DisplayDonationHeader": false,
"DisplayExternalContributors": false,
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
@@ -708,9 +597,6 @@
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
"InternalAuthSettings": {
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell",
"LDAPSettings": {
@@ -796,7 +682,6 @@
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
"ResourceControl": null,
"Status": 1,
@@ -819,7 +704,6 @@
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
"ResourceControl": null,
"Status": 1,
@@ -842,7 +726,6 @@
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
"ResourceControl": null,
"Status": 1,
@@ -919,7 +802,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "70",
"DB_VERSION": "35",
"INSTANCE_ID": "null"
}
}

View File

@@ -1,118 +0,0 @@
package demo
import (
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
type EnvironmentDetails struct {
Enabled bool `json:"enabled"`
Users []portainer.UserID `json:"users"`
Environments []portainer.EndpointID `json:"environments"`
}
type Service struct {
details EnvironmentDetails
}
func NewService() *Service {
return &Service{}
}
func (service *Service) Details() EnvironmentDetails {
return service.details
}
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
log.Print("[INFO] [main] Starting demo environment")
isClean, err := isCleanStore(store)
if err != nil {
return errors.WithMessage(err, "failed checking if store is clean")
}
if !isClean {
return errors.New(" Demo environment can only be initialized on a clean database")
}
id, err := initDemoUser(store, cryptoService)
if err != nil {
return errors.WithMessage(err, "failed creating demo user")
}
endpointIds, err := initDemoEndpoints(store)
if err != nil {
return errors.WithMessage(err, "failed creating demo endpoint")
}
err = initDemoSettings(store)
if err != nil {
return errors.WithMessage(err, "failed updating demo settings")
}
service.details = EnvironmentDetails{
Enabled: true,
Users: []portainer.UserID{id},
// endpoints 2,3 are created after deployment of portainer
Environments: endpointIds,
}
return nil
}
func isCleanStore(store dataservices.DataStore) (bool, error) {
endpoints, err := store.Endpoint().Endpoints()
if err != nil {
return false, err
}
if len(endpoints) > 0 {
return false, nil
}
users, err := store.User().Users()
if err != nil {
return false, err
}
if len(users) > 0 {
return false, nil
}
return true, nil
}
func (service *Service) IsDemo() bool {
return service.details.Enabled
}
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
if !service.IsDemo() {
return false
}
for _, demoEndpointID := range service.details.Environments {
if environmentID == demoEndpointID {
return true
}
}
return false
}
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
if !service.IsDemo() {
return false
}
for _, demoUserID := range service.details.Users {
if userID == demoUserID {
return true
}
}
return false
}

View File

@@ -1,79 +0,0 @@
package demo
import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
func initDemoUser(
store dataservices.DataStore,
cryptoService portainer.CryptoService,
) (portainer.UserID, error) {
password, err := cryptoService.Hash("tryportainer")
if err != nil {
return 0, errors.WithMessage(err, "failed creating password hash")
}
admin := &portainer.User{
Username: "admin",
Password: password,
Role: portainer.AdministratorRole,
}
err = store.User().Create(admin)
return admin.ID, errors.WithMessage(err, "failed creating user")
}
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
localEndpointId, err := initDemoLocalEndpoint(store)
if err != nil {
return nil, err
}
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
}
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
localEndpoint := &portainer.Endpoint{
ID: id,
Name: "local",
URL: "unix:///var/run/docker.sock",
PublicURL: "demo.portainer.io",
Type: portainer.DockerEnvironment,
GroupID: portainer.EndpointGroupID(1),
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
err := store.Endpoint().Create(localEndpoint)
return id, errors.WithMessage(err, "failed creating local endpoint")
}
func initDemoSettings(
store dataservices.DataStore,
) error {
settings, err := store.Settings().Settings()
if err != nil {
return errors.WithMessage(err, "failed fetching settings")
}
settings.EnableTelemetry = false
settings.LogoURL = ""
err = store.Settings().UpdateSettings(settings)
return errors.WithMessage(err, "failed updating settings")
}

View File

@@ -7,10 +7,9 @@ import (
"time"
"github.com/docker/docker/api/types"
_container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
)
// Snapshotter represents a service used to create environment(endpoint) snapshots
@@ -155,35 +154,11 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
healthyContainers := 0
unhealthyContainers := 0
stacks := make(map[string]struct{})
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "exited" {
stoppedContainers++
} else if container.State == "running" {
runningContainers++
// snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
}
}
if strings.Contains(container.Status, "(healthy)") {
@@ -199,14 +174,6 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
}
}
gpuUseList := make([]string, 0, len(gpuUseSet))
for gpuUse := range gpuUseSet {
gpuUseList = append(gpuUseList, gpuUse)
}
snapshot.GpuUseAll = gpuUseAll
snapshot.GpuUseList = gpuUseList
snapshot.RunningContainerCount = runningContainers
snapshot.StoppedContainerCount = stoppedContainers
snapshot.HealthyContainerCount = healthyContainers

View File

@@ -6,6 +6,7 @@ import (
"io"
"os"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
@@ -13,6 +14,7 @@ import (
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
"github.com/docker/cli/cli/compose/loader"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
@@ -54,13 +56,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
return errors.Wrap(err, "failed to deploy a stack")
}
@@ -74,14 +76,12 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
if err := updateNetworkEnvFile(stack); err != nil {
return err
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
return errors.Wrap(err, "failed to remove a stack")
}
@@ -103,42 +103,200 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
}
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
func createEnvFile(stack *portainer.Stack) (string, error) {
// workaround for EE-1862. It will have to be removed when
// docker/compose upgraded to v2.x.
if err := createNetworkEnvFile(stack); err != nil {
return "", errors.Wrap(err, "failed to create network env file")
}
if stack.Env == nil || len(stack.Env) == 0 {
return "", nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
defer envfile.Close()
copyDefaultEnvFile(stack, envfile)
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return "stack.env", nil
}
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) {
defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env"))
if err != nil {
// If cannot open a default file, then don't need to copy it.
// We could as well stat it and check if it exists, but this is more efficient.
return
func fileNotExist(filePath string) bool {
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return true
}
defer defaultEnvFile.Close()
if _, err = io.Copy(w, defaultEnvFile); err == nil {
io.WriteString(w, "\n")
}
// If couldn't copy the .env file, then ignore the error and try to continue
return false
}
func updateNetworkEnvFile(stack *portainer.Stack) error {
envFilePath := path.Join(stack.ProjectPath, ".env")
stackFilePath := path.Join(stack.ProjectPath, "stack.env")
if fileNotExist(envFilePath) {
if fileNotExist(stackFilePath) {
return nil
}
flags := os.O_WRONLY | os.O_SYNC | os.O_CREATE
envFile, err := os.OpenFile(envFilePath, flags, 0666)
if err != nil {
return err
}
defer envFile.Close()
stackFile, err := os.Open(stackFilePath)
if err != nil {
return err
}
defer stackFile.Close()
_, err = io.Copy(envFile, stackFile)
return err
}
return nil
}
func createNetworkEnvFile(stack *portainer.Stack) error {
networkNameSet := NewStringSet()
for _, filePath := range stackutils.GetStackFilePaths(stack) {
networkNames, err := extractNetworkNames(filePath)
if err != nil {
return errors.Wrap(err, "failed to extract network name")
}
if networkNames == nil || networkNames.Len() == 0 {
continue
}
networkNameSet.Union(networkNames)
}
for _, s := range networkNameSet.List() {
if _, ok := os.LookupEnv(s); ok {
networkNameSet.Remove(s)
}
}
if networkNameSet.Len() == 0 && stack.Env == nil {
return nil
}
envfile, err := os.OpenFile(path.Join(stack.ProjectPath, ".env"),
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.Wrap(err, "failed to open env file")
}
defer envfile.Close()
var scanEnvSettingFunc = func(name string) (string, bool) {
if stack.Env != nil {
for _, v := range stack.Env {
if name == v.Name {
return v.Value, true
}
}
}
return "", false
}
for _, s := range networkNameSet.List() {
if _, ok := scanEnvSettingFunc(s); !ok {
stack.Env = append(stack.Env, portainer.Pair{
Name: s,
Value: "None",
})
}
}
if stack.Env != nil {
for _, v := range stack.Env {
envfile.WriteString(
fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
}
return nil
}
func extractNetworkNames(filePath string) (StringSet, error) {
if info, err := os.Stat(filePath); errors.Is(err,
os.ErrNotExist) || info.IsDir() {
return nil, nil
}
stackFileContent, err := os.ReadFile(filePath)
if err != nil {
return nil, errors.Wrap(err, "failed to open yaml file")
}
config, err := loader.ParseYAML(stackFileContent)
if err != nil {
// invalid stack file
return nil, errors.Wrap(err, "invalid stack file")
}
var version string
if _, ok := config["version"]; ok {
version, _ = config["version"].(string)
}
var networks map[string]interface{}
if value, ok := config["networks"]; ok {
if value == nil {
return nil, nil
}
if networks, ok = value.(map[string]interface{}); !ok {
return nil, nil
}
} else {
return nil, nil
}
networkContent, err := loader.LoadNetworks(networks, version)
if err != nil {
return nil, nil // skip the error
}
re := regexp.MustCompile(`^\$\{?([^\}]+)\}?$`)
networkNames := NewStringSet()
for _, v := range networkContent {
matched := re.FindAllStringSubmatch(v.Name, -1)
if matched != nil && matched[0] != nil {
if strings.Contains(matched[0][1], ":-") {
continue
}
if strings.Contains(matched[0][1], "?") {
continue
}
if strings.Contains(matched[0][1], "-") {
continue
}
networkNames.Add(matched[0][1])
}
}
if networkNames.Len() == 0 {
return nil, nil
}
return networkNames, nil
}

View File

@@ -11,7 +11,6 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
)
const composeFile = `version: "3.9"
@@ -42,8 +41,6 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
func Test_UpAndDown(t *testing.T) {
testhelpers.IntegrationTest(t)
stack, endpoint := setup(t)
w, err := NewComposeStackManager("", "", nil)

View File

@@ -65,22 +65,56 @@ func Test_createEnvFile(t *testing.T) {
}
}
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
func Test_createNetworkEnvFile(t *testing.T) {
dir := t.TempDir()
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
stack := &portainer.Stack{
buf := []byte(`
version: '3.6'
services:
nginx-example:
image: nginx:latest
networks:
default:
name: ${test}
driver: bridge
`)
if err := ioutil.WriteFile(path.Join(dir,
"docker-compose.yml"), buf, 0644); err != nil {
t.Fatalf("Failed to create yaml file: %s", err)
}
stackWithoutEnv := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{},
}
if err := createNetworkEnvFile(stackWithoutEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err := ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=None\n", string(content))
stackWithEnv := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{
{Name: "VAR1", Value: "NEW_VAL1"},
{Name: "VAR3", Value: "VAL3"},
{Name: "test", Value: "test-value"},
},
}
result, err := createEnvFile(stack)
assert.Equal(t, "stack.env", result)
assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
if err := createNetworkEnvFile(stackWithEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err = ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=test-value\n", string(content))
}

View File

@@ -108,12 +108,12 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
return "", errors.WithMessage(err, "failed to parse url")
}
rootItemUrl, err := a.buildRootItemUrl(config, options.referenceName)
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build azure root item url")
return "", errors.WithMessage(err, "failed to build azure refs url")
}
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
req, err := http.NewRequestWithContext(ctx, "GET", refsUrl, nil)
if options.username != "" || options.password != "" {
req.SetBasicAuth(options.username, options.password)
} else if config.username != "" || config.password != "" {
@@ -131,24 +131,26 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status)
return "", fmt.Errorf("failed to get repository refs with a status \"%v\"", resp.Status)
}
var items struct {
var refs struct {
Value []struct {
CommitId string `json:"commitId"`
Name string `json:"name"`
ObjectId string `json:"objectId"`
}
}
if err := json.NewDecoder(resp.Body).Decode(&refs); err != nil {
return "", errors.Wrap(err, "could not parse Azure Refs response")
}
for _, ref := range refs.Value {
if strings.EqualFold(ref.Name, options.referenceName) {
return ref.ObjectId, nil
}
}
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
return "", errors.Wrap(err, "could not parse Azure items response")
}
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
return "", errors.Errorf("failed to get latest commitID in the repository")
}
return items.Value[0].CommitId, nil
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
}
func parseUrl(rawUrl string) (*azureOptions, error) {
@@ -234,10 +236,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
q.Set("scopePath", "/")
q.Set("download", "true")
if referenceName != "" {
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
}
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
q.Set("$format", "zip")
q.Set("recursionLevel", "full")
q.Set("api-version", "6.0")
@@ -246,8 +246,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
return u.String(), nil
}
func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName string) (string, error) {
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/refs",
a.baseUrl,
url.PathEscape(config.organisation),
url.PathEscape(config.project),
@@ -255,15 +255,12 @@ func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName s
u, err := url.Parse(rawUrl)
if err != nil {
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
}
// filterContains=main&api-version=6.0
q := u.Query()
q.Set("scopePath", "/")
if referenceName != "" {
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
}
q.Set("filterContains", formatReferenceName(referenceName))
q.Set("api-version", "6.0")
u.RawQuery = q.Encode()

View File

@@ -28,15 +28,15 @@ func Test_buildDownloadUrl(t *testing.T) {
}
}
func Test_buildRootItemUrl(t *testing.T) {
func Test_buildRefsUrl(t *testing.T) {
a := NewAzureDownloader(nil)
u, err := a.buildRootItemUrl(&azureOptions{
u, err := a.buildRefsUrl(&azureOptions{
organisation: "organisation",
project: "project",
repository: "repository",
}, "refs/heads/main")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&api-version=6.0&versionDescriptor.version=main&versionDescriptor.versionType=branch")
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/refs?filterContains=main&api-version=6.0")
actualUrl, _ := url.Parse(u)
assert.NoError(t, err)
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
@@ -270,17 +270,63 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
func Test_azureDownloader_latestCommitID(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := `{
"count": 1,
"value": [
{
"objectId": "1a5630f017127db7de24d8771da0f536ff98fc9b",
"gitObjectType": "tree",
"commitId": "27104ad7549d9e66685e115a497533f18024be9c",
"path": "/",
"isFolder": true,
"url": "https://dev.azure.com/simonmeng0474/4b546a97-c481-4506-bdd5-976e9592f91a/_apis/git/repositories/a22247ad-053f-43bc-88a7-62ff4846bb97/items?path=%2F&versionType=Branch&versionOptions=None"
}
]
"value": [
{
"name": "refs/heads/feature/calcApp",
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2FcalcApp"
},
{
"name": "refs/heads/feature/replacer",
"objectId": "917131a709996c5cfe188c3b57e9a6ad90e8b85c",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2Freplacer"
},
{
"name": "refs/heads/master",
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
"creator": {
"displayName": "Normal Paulk",
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"_links": {
"avatar": {
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
}
},
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"uniqueName": "dev@mailserver.com",
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
},
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Fmaster"
}
],
"count": 3
}`
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(response))
@@ -301,11 +347,19 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
{
name: "should be able to parse response",
args: fetchOptions{
referenceName: "",
referenceName: "refs/heads/master",
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
want: "27104ad7549d9e66685e115a497533f18024be9c",
want: "ffe9cba521f00d7f60e322845072238635edb451",
wantErr: false,
},
{
name: "should be able to parse response",
args: fetchOptions{
referenceName: "refs/heads/unknown",
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {

View File

@@ -82,17 +82,8 @@ func (c gitClient) latestCommitID(ctx context.Context, opt fetchOptions) (string
return "", errors.Wrap(err, "failed to list repository refs")
}
referenceName := opt.referenceName
if referenceName == "" {
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), "HEAD") {
referenceName = ref.Target().String()
}
}
}
for _, ref := range refs {
if strings.EqualFold(ref.Name().String(), referenceName) {
if strings.EqualFold(ref.Name().String(), opt.referenceName) {
return ref.Hash().String(), nil
}
}

View File

@@ -1,6 +1,6 @@
module github.com/portainer/portainer/api
go 1.18
go 1.17
require (
github.com/Microsoft/go-winio v0.5.1
@@ -11,7 +11,7 @@ require (
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.16+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/fvbommel/sortorder v1.0.2
github.com/fxamacker/cbor/v2 v2.3.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
@@ -20,7 +20,7 @@ require (
github.com/go-playground/validator/v10 v10.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.8
github.com/google/go-cmp v0.5.6
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
@@ -32,7 +32,7 @@ require (
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/docker-compose-wrapper v0.0.0-20220407011010-3c7408969ad3
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
@@ -43,7 +43,6 @@ require (
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
@@ -62,6 +61,7 @@ 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/containerd/containerd v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -95,9 +95,6 @@ require (
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.15.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -115,11 +112,12 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,4 @@ var (
ErrUnauthorized = errors.New("Unauthorized")
// ErrResourceAccessDenied Access denied to resource error
ErrResourceAccessDenied = errors.New("Access denied to resource")
// ErrNotAvailableInDemo feature is not allowed in demo
ErrNotAvailableInDemo = errors.New("This feature is not available in the demo version of Portainer")
)

View File

@@ -13,6 +13,7 @@ import (
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/passwordutils"
)
type authenticatePayload struct {
@@ -100,7 +101,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
forceChangePassword := !handler.passwordStrengthChecker.Check(password)
forceChangePassword := !passwordutils.StrengthCheck(password)
return handler.writeToken(w, user, forceChangePassword)
}

View File

@@ -22,14 +22,12 @@ type Handler struct {
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
passwordStrengthChecker security.PasswordStrengthChecker
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler {
h := &Handler{
Router: mux.NewRouter(),
passwordStrengthChecker: passwordStrengthChecker,
Router: mux.NewRouter(),
}
h.Handle("/auth/oauth/validate",

View File

@@ -18,7 +18,6 @@ import (
"github.com/docker/docker/pkg/ioutils"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
@@ -50,7 +49,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
@@ -87,7 +86,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()

View File

@@ -9,8 +9,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/http/security"
)
@@ -27,17 +25,7 @@ type Handler struct {
}
// NewHandler creates an new instance of backup handler
func NewHandler(
bouncer *security.RequestBouncer,
dataStore dataservices.DataStore,
gate *offlinegate.OfflineGate,
filestorePath string,
shutdownTrigger context.CancelFunc,
adminMonitor *adminmonitor.Monitor,
demoService *demo.Service,
) *Handler {
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
h := &Handler{
Router: mux.NewRouter(),
bouncer: bouncer,
@@ -48,11 +36,8 @@ func NewHandler(
adminMonitor: adminMonitor,
}
demoRestrictedRouter := h.NewRoute().Subrouter()
demoRestrictedRouter.Use(middlewares.RestrictDemoEnv(demoService.IsDemo))
demoRestrictedRouter.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
demoRestrictedRouter.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
return h
}
@@ -65,7 +50,7 @@ func adminAccess(next http.Handler) http.Handler {
}
if !securityContext.IsAdmin {
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
}
next.ServeHTTP(w, r)

View File

@@ -14,7 +14,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
@@ -52,7 +51,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, test.backupPassword)
@@ -75,7 +74,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, "password")

View File

@@ -1,7 +1,6 @@
package customtemplates
import (
"encoding/json"
"errors"
"log"
"net/http"
@@ -116,8 +115,6 @@ type customTemplateFromFileContentPayload struct {
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
@@ -139,12 +136,6 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -173,7 +164,6 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p
Platform: (payload.Platform),
Type: (payload.Type),
Logo: payload.Logo,
Variables: payload.Variables,
}
templateFolder := strconv.Itoa(customTemplateID)
@@ -214,8 +204,6 @@ type customTemplateFromGitRepositoryPayload struct {
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
@@ -248,12 +236,6 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -274,7 +256,6 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
Platform: payload.Platform,
Type: payload.Type,
Logo: payload.Logo,
Variables: payload.Variables,
}
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
@@ -335,8 +316,6 @@ type customTemplateFromFileUploadPayload struct {
Platform portainer.CustomTemplatePlatform
Type portainer.StackType
FileContent []byte
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error {
@@ -382,17 +361,6 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
}
payload.FileContent = composeFileContent
varsString, _ := request.RetrieveMultiPartFormValue(r, "Variables", true)
err = json.Unmarshal([]byte(varsString), &payload.Variables)
if err != nil {
return errors.New("Invalid variables. Ensure that the variables are valid JSON")
}
err = validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -413,7 +381,6 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
Type: payload.Type,
Logo: payload.Logo,
EntryPoint: filesystem.ComposeFileDefaultName,
Variables: payload.Variables,
}
templateFolder := strconv.Itoa(customTemplateID)

View File

@@ -31,8 +31,6 @@ type customTemplateUpdatePayload struct {
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
// Content of stack file
FileContent string `validate:"required"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
}
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
@@ -54,12 +52,6 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
err := validateVariablesDefinitions(payload.Variables)
if err != nil {
return err
}
return nil
}
@@ -132,7 +124,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
customTemplate.Note = payload.Note
customTemplate.Platform = payload.Platform
customTemplate.Type = payload.Type
customTemplate.Variables = payload.Variables
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
if err != nil {

View File

@@ -1,19 +0,0 @@
package customtemplates
import (
"errors"
portainer "github.com/portainer/portainer/api"
)
func validateVariablesDefinitions(variables []portainer.CustomTemplateVariableDefinition) error {
for _, variable := range variables {
if variable.Name == "" {
return errors.New("variable name is required")
}
if variable.Label == "" {
return errors.New("variable label is required")
}
}
return nil
}

View File

@@ -1,86 +0,0 @@
package containers
import (
"net/http"
"strings"
containertypes "github.com/docker/docker/api/types/container"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"golang.org/x/exp/slices"
)
type containerGpusResponse struct {
Gpus string `json:"gpus"`
}
// @id dockerContainerGpusInspect
// @summary Fetch container gpus data
// @description
// @description **Access policy**:
// @tags docker
// @security jwt
// @accept json
// @produce json
// @param environmentId path int true "Environment identifier"
// @param containerId path int true "Container identifier"
// @success 200 {object} containerGpusResponse "Success"
// @failure 404 "Environment or container not found"
// @failure 400 "Bad request"
// @failure 500 "Internal server error"
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
if err != nil {
return httperror.BadRequest("Invalid container identifier route variable", err)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
}
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
if err != nil {
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
}
container, err := cli.ContainerInspect(r.Context(), containerId)
if err != nil {
return httperror.NotFound("Unable to find the container", err)
}
if container.HostConfig == nil {
return httperror.NotFound("Unable to find the container host config", err)
}
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
if opt.Driver == "nvidia" {
return true
}
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
return false
}
return opt.Capabilities[0][0] == "gpu"
})
if gpuOptionsIndex == -1 {
return response.JSON(w, containerGpusResponse{Gpus: "none"})
}
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
gpu := "all"
if gpuOptions.Count != -1 {
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
}
return response.JSON(w, containerGpusResponse{Gpus: gpu})
}

View File

@@ -1,31 +0,0 @@
package containers
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
)
type Handler struct {
*mux.Router
dockerClientFactory *docker.ClientFactory
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dockerClientFactory: dockerClientFactory,
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess)
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
return h
}

View File

@@ -1,63 +0,0 @@
package docker
import (
"errors"
"net/http"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/handler/docker/containers"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
dataStore dataservices.DataStore
dockerClientFactory *docker.ClientFactory
authorizationService *authorization.Service
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
authorizationService: authorizationService,
dataStore: dataStore,
dockerClientFactory: dockerClientFactory,
}
// endpoints
endpointRouter := h.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(dockerOnlyMiddleware)
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
return h
}
func dockerOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
return
}
if !endpointutils.IsDockerEndpoint(endpoint) {
errMessage := "environment is not a docker environment"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
return
}
next.ServeHTTP(rw, request)
})
}

View File

@@ -164,7 +164,9 @@ func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
for edgeStackID := range edgeStackSet {
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
}
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
}

View File

@@ -105,7 +105,6 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
DeploymentType: payload.DeploymentType,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
@@ -228,7 +227,6 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
Name: payload.Name,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
DeploymentType: payload.DeploymentType,
Version: 1,
}
@@ -337,7 +335,6 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
DeploymentType: payload.DeploymentType,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
@@ -411,7 +408,7 @@ func updateEndpointRelations(endpointRelationService dataservices.EndpointRelati
return fmt.Errorf("unable to find endpoint relation in database: %w", err)
}
relation.EdgeStacks[edgeStackID] = true
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
err = endpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {

View File

@@ -1,21 +1,14 @@
package edgestacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
/*
func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
edgeStackID := portainer.EdgeStackID(5)
endpointRelations := []portainer.EndpointRelation{
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]bool{}},
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]bool{}},
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]bool{}},
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]bool{}},
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]bool{}},
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
}
relatedIds := []portainer.EndpointID{2, 3}
@@ -36,3 +29,4 @@ func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
assert.Equal(t, shouldBeRelated, relation.EdgeStacks[edgeStackID])
}
}
*/

View File

@@ -5,6 +5,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
// @id EdgeStackList
@@ -25,5 +26,35 @@ func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
return response.JSON(w, edgeStacks)
endpointRels, err := handler.DataStore.EndpointRelation().EndpointRelations()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint relations from the database", err}
}
m := make(map[portainer.EdgeStackID]map[portainer.EndpointID]portainer.EdgeStackStatus)
for _, r := range endpointRels {
for edgeStackID, status := range r.EdgeStacks {
if m[edgeStackID] == nil {
m[edgeStackID] = make(map[portainer.EndpointID]portainer.EdgeStackStatus)
}
m[edgeStackID][r.EndpointID] = status
}
}
type EdgeStackWithStatus struct {
portainer.EdgeStack
Status map[portainer.EndpointID]portainer.EdgeStackStatus
}
var edgeStacksWS []EdgeStackWithStatus
for _, s := range edgeStacks {
edgeStacksWS = append(edgeStacksWS, EdgeStackWithStatus{
EdgeStack: s,
Status: m[s.ID],
})
}
return response.JSON(w, edgeStacksWS)
}

View File

@@ -53,12 +53,5 @@ func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Req
return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
}
delete(stack.Status, endpoint.ID)
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}

View File

@@ -49,13 +49,6 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
var payload updateStatusPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -74,17 +67,28 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
Type: *payload.Status,
Error: payload.Error,
EndpointID: *payload.EndpointID,
endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment relations", err}
}
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
endpointRelation.EdgeStacks[portainer.EdgeStackID(stackID)] = portainer.EdgeStackStatus{
Type: *payload.Status,
Error: payload.Error,
}
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
return response.JSON(w, stack)
}

View File

@@ -1,924 +0,0 @@
package edgestacks
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strconv"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
)
type gitService struct {
cloneErr error
id string
}
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
return g.cloneErr
}
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return g.id, nil
}
// Helpers
func setupHandler(t *testing.T) (*Handler, string, func()) {
t.Helper()
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
jwtService, err := jwt.NewService("1h", store)
if err != nil {
storeTeardown()
t.Fatal(err)
}
user := &portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole}
err = store.User().Create(user)
if err != nil {
storeTeardown()
t.Fatal(err)
}
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
rawAPIKey, _, err := apiKeyService.GenerateApiKey(*user, "test")
if err != nil {
storeTeardown()
t.Fatal(err)
}
handler := NewHandler(
security.NewRequestBouncer(store, jwtService, apiKeyService),
store,
)
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
if err != nil {
storeTeardown()
t.Fatal(err)
}
fs, err := filesystem.NewService(tmpDir, "")
if err != nil {
storeTeardown()
t.Fatal(err)
}
handler.FileService = fs
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
t.Fatal(err)
}
settings.EnableEdgeComputeFeatures = true
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
t.Fatal(err)
}
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
return handler, rawAPIKey, storeTeardown
}
func createEndpoint(t *testing.T, store dataservices.DataStore) portainer.Endpoint {
t.Helper()
endpointID := portainer.EndpointID(5)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
err := store.Endpoint().Create(&endpoint)
if err != nil {
t.Fatal(err)
}
return endpoint
}
func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID portainer.EndpointID) portainer.EdgeStack {
t.Helper()
edgeGroup := portainer.EdgeGroup{
ID: 1,
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{endpointID},
PartialMatch: false,
}
err := store.EdgeGroup().Create(&edgeGroup)
if err != nil {
t.Fatal(err)
}
edgeStackID := portainer.EdgeStackID(14)
edgeStack := portainer.EdgeStack{
ID: edgeStackID,
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpointID},
},
CreationDate: time.Now().Unix(),
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
ProjectPath: "/project/path",
EntryPoint: "entrypoint",
Version: 237,
ManifestPath: "/manifest/path",
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpointID,
EdgeStacks: map[portainer.EdgeStackID]bool{
edgeStack.ID: true,
},
}
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
if err != nil {
t.Fatal(err)
}
err = store.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
return edgeStack
}
// Inspect
func TestInspectInvalidEdgeID(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
cases := []struct {
Name string
EdgeStackID string
ExpectedStatusCode int
}{
{"Invalid EdgeStackID", "x", 400},
{"Non-existing EdgeStackID", "5", 404},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/edge_stacks/"+tc.EdgeStackID, nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Create
func TestCreateAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
// Create Endpoint, EdgeGroup and EndpointRelation
endpoint := createEndpoint(t, handler.DataStore)
edgeGroup := portainer.EdgeGroup{
ID: 1,
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{endpoint.ID},
PartialMatch: false,
}
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
if err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
payload := swarmStackFromFileContentPayload{
Name: "Test Stack",
StackFileContent: "stack content",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, "/edge_stacks?method=string", r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data = portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if payload.Name != data.Name {
t.Fatalf(fmt.Sprintf("expected EdgeStack Name %s, found %s", payload.Name, data.Name))
}
}
func TestCreateWithInvalidPayload(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
cases := []struct {
Name string
Payload interface{}
QueryString string
ExpectedStatusCode int
}{
{
Name: "Invalid query string parameter",
Payload: swarmStackFromFileContentPayload{},
QueryString: "invalid=query-string",
ExpectedStatusCode: 400,
},
{
Name: "Invalid creation method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=invalid-creation-method",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with string method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with repository method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=repository",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with file method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=file",
ExpectedStatusCode: 500,
},
{
Name: "Duplicated EdgeStack Name",
Payload: swarmStackFromFileContentPayload{
Name: edgeStack.Name,
StackFileContent: "content",
EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty EdgeStack Groups",
Payload: swarmStackFromFileContentPayload{
Name: edgeStack.Name,
StackFileContent: "content",
EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
Payload: swarmStackFromFileContentPayload{
Name: "Stack name",
StackFileContent: "content",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Empty Stack File Content",
Payload: swarmStackFromFileContentPayload{
Name: "Stack name",
StackFileContent: "",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=string",
ExpectedStatusCode: 500,
},
{
Name: "Clone Git respository error",
Payload: swarmStackFromGitRepositoryPayload{
Name: "Stack name",
RepositoryURL: "github.com/portainer/portainer",
RepositoryReferenceName: "ref name",
RepositoryAuthentication: false,
RepositoryUsername: "",
RepositoryPassword: "",
FilePathInRepository: "/file/path",
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=repository",
ExpectedStatusCode: 500,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks?%s", tc.QueryString), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Delete
func TestDeleteAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
// Create
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Inspect
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.ID != edgeStack.ID {
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID))
}
// Delete
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNoContent, rec.Code))
}
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNotFound, rec.Code))
}
}
func TestDeleteInvalidEdgeStack(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
cases := []struct {
Name string
URL string
ExpectedStatusCode int
}{
{Name: "Non-existing EdgeStackID", URL: "/edge_stacks/-1", ExpectedStatusCode: http.StatusNotFound},
{Name: "Invalid EdgeStackID", URL: "/edge_stacks/aaaaaaa", ExpectedStatusCode: http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodDelete, tc.URL, nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Update
func TestUpdateAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack: create new Endpoint, EndpointRelation and EdgeGroup
endpointID := portainer.EndpointID(6)
newEndpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
}
err := handler.DataStore.Endpoint().Create(&newEndpoint)
if err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpointID,
EdgeStacks: map[portainer.EdgeStackID]bool{
edgeStack.ID: true,
},
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
if err != nil {
t.Fatal(err)
}
newEdgeGroup := portainer.EdgeGroup{
ID: 2,
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{newEndpoint.ID},
PartialMatch: false,
}
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
if err != nil {
t.Fatal(err)
}
newVersion := 238
payload := updateEdgeStackPayload{
StackFileContent: "update-test",
Version: &newVersion,
EdgeGroups: append(edgeStack.EdgeGroups, newEdgeGroup.ID),
DeploymentType: portainer.EdgeStackDeploymentCompose,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.Version != *payload.Version {
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", edgeStack.Version, data.Version))
}
if data.DeploymentType != payload.DeploymentType {
t.Fatalf(fmt.Sprintf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, data.DeploymentType))
}
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
t.Fatalf("expected EdgeGroups to be equal")
}
}
func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
//newEndpoint := createEndpoint(t, handler.DataStore)
newEdgeGroup := portainer.EdgeGroup{
ID: 2,
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
Endpoints: []portainer.EndpointID{8889},
PartialMatch: false,
}
handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
newVersion := 238
cases := []struct {
Name string
Payload updateEdgeStackPayload
ExpectedStatusCode int
}{
{
"Update with non-existing EdgeGroupID",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{9999},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusInternalServerError,
},
{
"Update with invalid EdgeGroup (non-existing Endpoint)",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{2},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusInternalServerError,
},
{
"Update DeploymentType from Docker to Kubernetes",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
},
http.StatusBadRequest,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
func TestUpdateWithInvalidPayload(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
newVersion := 238
cases := []struct {
Name string
Payload updateEdgeStackPayload
ExpectedStatusCode int
}{
{
"Update with empty StackFileContent",
updateEdgeStackPayload{
StackFileContent: "",
Version: &newVersion,
EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType,
},
http.StatusBadRequest,
},
{
"Update with empty EdgeGroups",
updateEdgeStackPayload{
StackFileContent: "error-test",
Version: &newVersion,
EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType,
},
http.StatusBadRequest,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Update Status
func TestUpdateStatusAndInspect(t *testing.T) {
handler, rawAPIKey, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack status
newStatus := portainer.StatusError
payload := updateStatusPayload{
Error: "test-error",
Status: &newStatus,
EndpointID: &endpoint.ID,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.Status[endpoint.ID].Type != *payload.Status {
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusType %d, found %d", payload.Status, data.Status[endpoint.ID].Type))
}
if data.Status[endpoint.ID].Error != payload.Error {
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusError %s, found %s", payload.Error, data.Status[endpoint.ID].Error))
}
if data.Status[endpoint.ID].EndpointID != *payload.EndpointID {
t.Fatalf(fmt.Sprintf("expected EndpointID %d, found %d", payload.EndpointID, data.Status[endpoint.ID].EndpointID))
}
}
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
handler, _, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Update edge stack status
statusError := portainer.StatusError
statusOk := portainer.StatusOk
cases := []struct {
Name string
Payload updateStatusPayload
ExpectedErrorMessage string
ExpectedStatusCode int
}{
{
"Update with nil Status",
updateStatusPayload{
Error: "test-error",
Status: nil,
EndpointID: &endpoint.ID,
},
"Invalid status",
400,
},
{
"Update with error status and empty error message",
updateStatusPayload{
Error: "",
Status: &statusError,
EndpointID: &endpoint.ID,
},
"Error message is mandatory when status is error",
400,
},
{
"Update with nil EndpointID",
updateStatusPayload{
Error: "",
Status: &statusOk,
EndpointID: nil,
},
"Invalid EnvironmentID",
400,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.ExpectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
}
})
}
}
// Delete Status
func TestDeleteStatus(t *testing.T) {
handler, _, teardown := setupHandler(t)
defer teardown()
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
}
}

View File

@@ -2,10 +2,11 @@ package edgestacks
import (
"errors"
"github.com/portainer/portainer/api/internal/endpointutils"
"net/http"
"strconv"
"github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -118,7 +119,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
}
relation.EdgeStacks[stack.ID] = true
relation.EdgeStacks[stack.ID] = portainer.EdgeStackStatus{}
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
@@ -180,7 +181,6 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
if payload.Version != nil && *payload.Version != stack.Version {
stack.Version = *payload.Version
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
}
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)

View File

@@ -77,16 +77,13 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
}
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
}
endpoint.Type = agentPlatform
version := r.Header.Get(portainer.PortainerAgentHeader)
endpoint.Agent.Version = version
endpoint.LastCheckInDate = time.Now().Unix()

View File

@@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
portainer.EndpointRelation{
EndpointID: 2,
},
http.StatusForbidden,
http.StatusBadRequest,
},
{
portainer.Endpoint{
@@ -194,9 +194,7 @@ func TestWithEndpoints(t *testing.T) {
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -241,7 +239,6 @@ func TestLastCheckInDateIncreases(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -306,6 +303,7 @@ func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
}
/*
func TestEdgeStackStatus(t *testing.T) {
handler, teardown, err := setupHandler()
defer teardown()
@@ -358,7 +356,6 @@ func TestEdgeStackStatus(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -377,6 +374,7 @@ func TestEdgeStackStatus(t *testing.T) {
assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
}
*/
func TestEdgeJobsResponse(t *testing.T) {
handler, teardown, err := setupHandler()
@@ -428,7 +426,6 @@ func TestEdgeJobsResponse(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

View File

@@ -35,11 +35,12 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en
}
endpointStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
updatedStacks := make(map[portainer.EdgeStackID]portainer.EdgeStackStatus)
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
updatedStacks[edgeStackID] = endpointRelation.EdgeStacks[edgeStackID]
}
endpointRelation.EdgeStacks = stacksSet
endpointRelation.EdgeStacks = updatedStacks
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
}

View File

@@ -1,50 +0,0 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/set"
)
// @id AgentVersions
// @summary List agent versions
// @description List all agent versions based on the current user authorizations and query parameters.
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} string "List of available agent versions"
// @failure 500 "Server error"
// @router /endpoints/agent_versions [get]
func (handler *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
agentVersions := set.Set[string]{}
for _, endpoint := range filteredEndpoints {
if endpoint.Agent.Version != "" {
agentVersions[endpoint.Agent.Version] = true
}
}
return response.JSON(w, agentVersions.Keys())
}

View File

@@ -23,7 +23,7 @@ import (
// @tags endpoints
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Success"
// @success 200 {object} portainer.Endpoint "Success"
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
@@ -61,7 +61,7 @@ func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http
handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID)
return response.Empty(w)
return response.JSON(w, endpoint)
}
func (handler *Handler) updateEdgeKey(edgeKey string) (string, error) {

View File

@@ -1,19 +1,20 @@
package endpoints
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"
"github.com/gofrs/uuid"
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/agent"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/edge"
@@ -24,7 +25,6 @@ type endpointCreatePayload struct {
URL string
EndpointCreationType endpointCreationEnum
PublicURL string
Gpus []portainer.Pair
GroupID int
TLS bool
TLSSkipVerify bool
@@ -142,13 +142,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
payload.PublicURL = publicURL
}
gpus := make([]portainer.Pair, 0)
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
if err != nil {
return errors.New("Invalid Gpus parameter")
}
payload.Gpus = gpus
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
payload.EdgeCheckinInterval = checkinInterval
@@ -194,15 +187,6 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
isUnique, err := handler.isNameUnique(payload.Name, 0)
if err != nil {
return httperror.InternalServerError("Unable to check if name is unique", err)
}
if !isUnique {
return httperror.NewError(http.StatusConflict, "Name is not unique", nil)
}
endpoint, endpointCreationError := handler.createEndpoint(payload)
if endpointCreationError != nil {
return endpointCreationError
@@ -225,13 +209,15 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
relationObject := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, stackID := range relatedEdgeStacks {
relationObject.EdgeStacks[stackID] = true
relationObject.EdgeStacks[stackID] = portainer.EdgeStackStatus{
Type: portainer.StatusAcknowledged,
}
}
}
@@ -244,7 +230,6 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
var err error
switch payload.EndpointCreationType {
case azureEnvironment:
return handler.createAzureEndpoint(payload)
@@ -257,22 +242,12 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
endpointType := portainer.DockerEnvironment
var agentVersion string
if payload.EndpointCreationType == agentEnvironment {
var tlsConfig *tls.Config
if payload.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return nil, httperror.InternalServerError("Unable to create TLS configuration", err)
}
}
agentPlatform, version, err := agent.GetAgentVersionAndPlatform(payload.URL, tlsConfig)
agentPlatform, err := handler.pingAndCheckPlatform(payload)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
}
agentVersion = version
if agentPlatform == portainer.AgentPlatformDocker {
endpointType = portainer.AgentOnDockerEnvironment
} else if agentPlatform == portainer.AgentPlatformKubernetes {
@@ -282,7 +257,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
if payload.TLS {
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
return handler.createTLSSecuredEndpoint(payload, endpointType)
}
return handler.createUnsecuredEndpoint(payload)
}
@@ -308,7 +283,6 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
AzureCredentials: credentials,
@@ -327,31 +301,30 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
}
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
//endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
portainerHost, err := edge.ParseHostForEdge(payload.URL)
if err != nil {
return nil, httperror.BadRequest("Unable to parse host", err)
}
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
//edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
//ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
EdgeKey: edgeKey,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
//EdgeKey: edgeKey,
EdgeCheckinInterval: payload.EdgeCheckinInterval,
Kubernetes: portainer.KubernetesDefault(),
IsEdgeDevice: payload.IsEdgeDevice,
@@ -372,7 +345,15 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
endpoint.EdgeID = edgeID.String()
}
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
err = handler.saveEndpointAndUpdateAuthorizationsWithCallback(endpoint, func(id uint64) (int, interface{}) {
endpoint.ID = portainer.EndpointID(id)
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
endpoint.EdgeKey = handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, int(id))
}
return int(id), endpoint
})
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
}
@@ -398,7 +379,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -433,7 +413,6 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
Type: portainer.KubernetesLocalEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -454,7 +433,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
return endpoint, nil
}
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
@@ -463,7 +442,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -477,8 +455,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
IsEdgeDevice: payload.IsEdgeDevice,
}
endpoint.Agent.Version = agentVersion
err := handler.storeTLSFiles(endpoint, payload)
if err != nil {
return nil, err
@@ -545,6 +521,42 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
return nil
}
func (handler *Handler) saveEndpointAndUpdateAuthorizationsWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
err := handler.DataStore.Endpoint().CreateWithCallback(endpoint, fn)
if err != nil {
return err
}
for _, tagID := range endpoint.TagIDs {
tag, err := handler.DataStore.Tag().Tag(tagID)
if err != nil {
return err
}
tag.Endpoints[endpoint.ID] = true
err = handler.DataStore.Tag().UpdateTag(tagID, tag)
if err != nil {
return err
}
}
return nil
}
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
folder := strconv.Itoa(int(endpoint.ID))
@@ -572,3 +584,58 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
return nil
}
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if payload.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return 0, err
}
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
if err != nil {
return 0, err
}
url.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
if err != nil {
return 0, err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, err
}
if agentPlatformNumber == 0 {
return 0, errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), nil
}

View File

@@ -12,7 +12,6 @@ import (
func TestEmptyGlobalKey(t *testing.T) {
handler := NewHandler(
helper.NewTestRequestBouncer(),
nil,
)
req, err := http.NewRequest(http.MethodPost, "https://portainer.io:9443/endpoints/global-key", nil)

View File

@@ -8,7 +8,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
)
// @id EndpointDelete
@@ -30,10 +29,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
}
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(endpointID)) {
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
@@ -92,22 +87,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
}
}
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
err = handler.DataStore.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err}
}
}
}
registries, err := handler.DataStore.Registry().Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}

View File

@@ -4,14 +4,24 @@ import (
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/utils"
)
const (
EdgeDeviceFilterAll = "all"
EdgeDeviceFilterTrusted = "trusted"
EdgeDeviceFilterUntrusted = "untrusted"
EdgeDeviceFilterNone = "none"
)
const (
@@ -19,10 +29,12 @@ const (
EdgeDeviceIntervalAdd = 20
)
var endpointGroupNames map[portainer.EndpointGroupID]string
// @id EndpointList
// @summary List environments(endpoints)
// @description List all environments(endpoints) based on the current user authorizations. Will
// @description return all environments(endpoints) if using an administrator or team leader account otherwise it will
// @description return all environments(endpoints) if using an administrator account otherwise it will
// @description only return authorized environments(endpoints).
// @description **Access policy**: restricted
// @tags endpoints
@@ -30,21 +42,14 @@ const (
// @security jwt
// @produce json
// @param start query int false "Start searching from"
// @param limit query int false "Limit results to this value"
// @param sort query int false "Sort results by this value"
// @param order query int false "Order sorted results by desc/asc" Enum("asc", "desc")
// @param search query string false "Search query"
// @param groupIds query []int false "List environments(endpoints) of these groups"
// @param status query []int false "List environments(endpoints) by this status"
// @param groupId query int false "List environments(endpoints) of this group"
// @param limit query int false "Limit results to this value"
// @param types query []int false "List environments(endpoints) of this type"
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)"
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
// @param agentVersions query []string false "will return only environments with on of these agent versions"
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
// @param name query string false "will return only environments(endpoints) with this name"
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
// @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error"
// @router /endpoints [get]
@@ -54,43 +59,105 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
start--
}
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
var endpointTypes []int
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
var tagIDs []portainer.TagID
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
var endpointIDs []portainer.EndpointID
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
var statuses []int
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
var groupIDs []int
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
}
// create endpoint groups as a map for more convenient access
endpointGroupNames = make(map[portainer.EndpointGroupID]string, 0)
for _, group := range endpointGroups {
endpointGroupNames[group.ID] = group.Name
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
query, err := parseQuery(r)
if err != nil {
return httperror.BadRequest("Invalid query parameters", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
totalAvailableEndpoints := len(filteredEndpoints)
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
if err != nil {
return httperror.InternalServerError("Unable to filter endpoints", err)
if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
}
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if len(groupIDs) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
}
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilter != "" {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
}
if len(statuses) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
}
if search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointTypes != nil {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
}
if tagIDs != nil {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
}
// Sort endpoints by field
sortEndpointsByField(filteredEndpoints, sortField, sortOrder == "desc")
filteredEndpointCount := len(filteredEndpoints)
@@ -129,7 +196,65 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
return endpoints[start:end]
}
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if utils.Contains(statuses, int(status)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func sortEndpointsByField(endpoints []portainer.Endpoint, sortField string, isSortDesc bool) {
switch sortField {
case "Name":
@@ -140,20 +265,10 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
case "Group":
endpointGroupNames := make(map[portainer.EndpointGroupID]string, 0)
for _, group := range endpointGroups {
endpointGroupNames[group.ID] = group.Name
}
endpointsByGroup := EndpointsByGroup{
endpointGroupNames: endpointGroupNames,
endpoints: endpoints,
}
if isSortDesc {
sort.Stable(sort.Reverse(endpointsByGroup))
sort.Stable(sort.Reverse(EndpointsByGroup(endpoints)))
} else {
sort.Stable(endpointsByGroup)
sort.Stable(EndpointsByGroup(endpoints))
}
case "Status":
@@ -169,6 +284,123 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
// none - return all endpoints that are not edge devices
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
return true
}
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return false
}
switch edgeDeviceFilter {
case EdgeDeviceFilterAll:
return true
case EdgeDeviceFilterTrusted:
return endpoint.UserTrusted
case EdgeDeviceFilterUntrusted:
return !endpoint.UserTrusted
}
return false
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
var endpointGroup portainer.EndpointGroup
for _, group := range groups {
@@ -179,3 +411,57 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
}
return endpointGroup
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}

View File

@@ -16,147 +16,66 @@ import (
"github.com/stretchr/testify/assert"
)
type endpointListTest struct {
type endpointListEdgeDeviceTest struct {
title string
expected []portainer.EndpointID
filter string
}
func Test_EndpointList_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{
ID: 1,
GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{
Version: "1.0.0",
},
}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1, Type: portainer.AgentOnDockerEnvironment, Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
handler, teardown := setup(t, []portainer.Endpoint{
notAgentEnvironments,
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
})
func Test_endpointList(t *testing.T) {
var err error
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
type endpointListAgentVersionTest struct {
endpointListTest
filter []string
}
tests := []endpointListAgentVersionTest{
{
endpointListTest{
"should show version 1 agent endpoints and non-agent endpoints",
[]portainer.EndpointID{version1Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version1Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version2Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 1 and 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID, version1Endpoint.ID},
},
[]string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
query := ""
for _, filter := range test.filter {
query += fmt.Sprintf("agentVersions[]=%s&", filter)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
respIds := []portainer.EndpointID{}
for _, endpoint := range resp {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
})
}
}
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
handler, teardown := setup(t, []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
endpoints := []portainer.Endpoint{
trustedEndpoint,
untrustedEndpoint,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
})
defer teardown()
type endpointListEdgeDeviceTest struct {
endpointListTest
edgeDevice *bool
edgeDeviceUntrusted bool
}
for _, endpoint := range endpoints {
err = store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
h := NewHandler(bouncer)
h.DataStore = store
h.ComposeStackManager = testhelpers.NewComposeStackManager()
tests := []endpointListEdgeDeviceTest{
{
endpointListTest: endpointListTest{
"should show all endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
},
edgeDevice: nil,
"should show all edge endpoints",
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterAll,
},
{
endpointListTest: endpointListTest{
"should show only trusted edge devices and regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
"should show only trusted edge devices",
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterTrusted,
},
{
endpointListTest: endpointListTest{
"should show only untrusted edge devices and regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
"should show only untrusted edge devices",
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
EdgeDeviceFilterUntrusted,
},
{
endpointListTest: endpointListTest{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
},
edgeDevice: BoolAddr(false),
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterNone,
},
}
@@ -164,13 +83,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted)
if test.edgeDevice != nil {
query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
req := buildEndpointListRequest(test.filter)
resp, err := doEndpointListRequest(req, h, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
@@ -186,28 +100,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
}
}
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}
func buildEndpointListRequest(query string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
func buildEndpointListRequest(filter string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)

View File

@@ -55,7 +55,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {

View File

@@ -47,7 +47,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {

View File

@@ -22,8 +22,6 @@ type endpointUpdatePayload struct {
// URL or IP address where exposed containers will be reachable.\
// Defaults to URL if not specified
PublicURL *string `example:"docker.mydomain.tld:2375"`
// GPUs information
Gpus []portainer.Pair
// Group identifier
GroupID *int `example:"1"`
// Require TLS to connect against this environment(endpoint)
@@ -90,18 +88,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.Name != nil {
name := *payload.Name
isUnique, err := handler.isNameUnique(name, endpoint.ID)
if err != nil {
return httperror.InternalServerError("Unable to check if name is unique", err)
}
if !isUnique {
return httperror.NewError(http.StatusConflict, "Name is not unique", nil)
}
endpoint.Name = name
endpoint.Name = *payload.Name
}
if payload.URL != nil {
@@ -112,10 +99,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL
}
if payload.Gpus != nil {
endpoint.Gpus = payload.Gpus
}
if payload.EdgeCheckinInterval != nil {
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
}
@@ -271,7 +254,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if (payload.URL != nil && *payload.URL != endpoint.URL) || (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) || endpoint.Type == portainer.AzureEnvironment {
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
@@ -321,7 +304,9 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
currentEdgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = currentEdgeStackSet
for edgeStackID := range currentEdgeStackSet {
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
}
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
if err != nil {

View File

@@ -1,435 +0,0 @@
package endpoints
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"golang.org/x/exp/slices"
)
type EnvironmentsQuery struct {
search string
types []portainer.EndpointType
tagIds []portainer.TagID
endpointIds []portainer.EndpointID
tagsPartialMatch bool
groupIds []portainer.EndpointGroupID
status []portainer.EndpointStatus
edgeDevice *bool
edgeDeviceUntrusted bool
name string
agentVersions []string
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
status, err := getNumberArrayQueryParameter[portainer.EndpointStatus](r, "status")
if err != nil {
return EnvironmentsQuery{}, err
}
groupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "groupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
endpointTypes, err := getNumberArrayQueryParameter[portainer.EndpointType](r, "types")
if err != nil {
return EnvironmentsQuery{}, err
}
tagIDs, err := getNumberArrayQueryParameter[portainer.TagID](r, "tagIds")
if err != nil {
return EnvironmentsQuery{}, err
}
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
endpointIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "endpointIds")
if err != nil {
return EnvironmentsQuery{}, err
}
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
var edgeDevice *bool
if edgeDeviceParam != "" {
edgeDevice = BoolAddr(edgeDeviceParam == "true")
}
edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true)
return EnvironmentsQuery{
search: search,
types: endpointTypes,
tagIds: tagIDs,
endpointIds: endpointIDs,
tagsPartialMatch: tagsPartialMatch,
groupIds: groupIDs,
status: status,
edgeDevice: edgeDevice,
edgeDeviceUntrusted: edgeDeviceUntrusted,
name: name,
agentVersions: agentVersions,
}, nil
}
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
totalAvailableEndpoints := len(filteredEndpoints)
if len(query.endpointIds) > 0 {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
}
if len(query.groupIds) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
if query.name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
}
if query.edgeDevice != nil {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted)
} else {
// If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpoint.IsEdgeDevice || endpoint.UserTrusted
})
}
if len(query.status) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
}
if query.search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
}
if len(query.types) > 0 {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, query.types)
}
if len(query.tagIds) > 0 {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
}
if len(query.agentVersions) > 0 {
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpointutils.IsAgentEndpoint(&endpoint) || contains(query.agentVersions, endpoint.Agent.Version)
})
}
return filteredEndpoints, totalAvailableEndpoints, nil
}
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if slices.Contains(statuses, status) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return true
}
if !edgeDeviceParam {
return !endpoint.IsEdgeDevice
}
return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
if name == "" {
return endpoints
}
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Name == name {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if predicate(endpoint) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getArrayQueryParameter(r *http.Request, parameter string) []string {
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
if !exists {
list = []string{}
}
return list
}
func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]T, error) {
list := getArrayQueryParameter(r, parameter)
if list == nil {
return []T{}, nil
}
var result []T
for _, item := range list {
number, err := strconv.Atoi(item)
if err != nil {
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
}
result = append(result, T(number))
}
return result, nil
}
func contains(strings []string, param string) bool {
for _, str := range strings {
if str == param {
return true
}
}
return false
}

View File

@@ -1,177 +0,0 @@
package endpoints
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
type filterTest struct {
title string
expected []portainer.EndpointID
query EnvironmentsQuery
}
func Test_Filter_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "1.0.0"}}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
endpoints := []portainer.Endpoint{
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
notAgentEnvironments,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show version 1 endpoints",
[]portainer.EndpointID{version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 1 and 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
}
runTests(tests, t, handler, endpoints)
}
func Test_Filter_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
endpoints := []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show all edge endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
},
},
{
"should show only trusted edge devices and other regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
},
},
{
"should show only untrusted edge devices and other regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
},
},
{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(false),
},
},
}
runTests(tests, t, handler, endpoints)
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
runTest(t, test, handler, endpoints)
})
}
}
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
is := assert.New(t)
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
is.NoError(err)
is.Equal(len(test.expected), len(filteredEndpoints))
respIds := []portainer.EndpointID{}
for _, endpoint := range filteredEndpoints {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
}
func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}

View File

@@ -4,7 +4,6 @@ import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/kubernetes/cli"
@@ -36,7 +35,6 @@ type requestBouncer interface {
type Handler struct {
*mux.Router
requestBouncer requestBouncer
demoService *demo.Service
DataStore dataservices.DataStore
FileService portainer.FileService
ProxyManager *proxy.Manager
@@ -50,11 +48,10 @@ type Handler struct {
}
// NewHandler creates a handler to manage environment(endpoint) operations.
func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
func NewHandler(bouncer requestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
demoService: demoService,
}
h.Handle("/endpoints",
@@ -67,9 +64,6 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/agent_versions",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",

View File

@@ -21,26 +21,23 @@ func (e EndpointsByName) Less(i, j int) bool {
return sortorder.NaturalLess(strings.ToLower(e[i].Name), strings.ToLower(e[j].Name))
}
type EndpointsByGroup struct {
endpointGroupNames map[portainer.EndpointGroupID]string
endpoints []portainer.Endpoint
}
type EndpointsByGroup []portainer.Endpoint
func (e EndpointsByGroup) Len() int {
return len(e.endpoints)
return len(e)
}
func (e EndpointsByGroup) Swap(i, j int) {
e.endpoints[i], e.endpoints[j] = e.endpoints[j], e.endpoints[i]
e[i], e[j] = e[j], e[i]
}
func (e EndpointsByGroup) Less(i, j int) bool {
if e.endpoints[i].GroupID == e.endpoints[j].GroupID {
if e[i].GroupID == e[j].GroupID {
return false
}
groupA := e.endpointGroupNames[e.endpoints[i].GroupID]
groupB := e.endpointGroupNames[e.endpoints[j].GroupID]
groupA := endpointGroupNames[e[i].GroupID]
groupB := endpointGroupNames[e[j].GroupID]
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
}

View File

@@ -1,18 +0,0 @@
package endpoints
import portainer "github.com/portainer/portainer/api"
func (handler *Handler) isNameUnique(name string, endpointID portainer.EndpointID) (bool, error) {
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return false, err
}
for _, endpoint := range endpoints {
if endpoint.Name == name && (endpointID == 0 || endpoint.ID != endpointID) {
return false, nil
}
}
return true, nil
}

View File

@@ -1,6 +0,0 @@
package endpoints
func BoolAddr(b bool) *bool {
boolVar := b
return &boolVar
}

View File

@@ -2,12 +2,13 @@ package handler
import (
"net/http"
"net/http/pprof"
"runtime"
"strings"
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates"
"github.com/portainer/portainer/api/http/handler/docker"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgejobs"
"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -46,7 +47,6 @@ type Handler struct {
AuthHandler *auth.Handler
BackupHandler *backup.Handler
CustomTemplatesHandler *customtemplates.Handler
DockerHandler *docker.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeJobsHandler *edgejobs.Handler
EdgeStacksHandler *edgestacks.Handler
@@ -82,7 +82,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.16.0
// @version 2.13.0
// @description.markdown api-description.md
// @termsOfService
@@ -156,9 +156,20 @@ type Handler struct {
// @tag.name websocket
// @tag.description Create exec sessions using websockets
func init() {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
}
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/debug/pprof/profile"):
pprof.Profile(w, r)
case strings.HasPrefix(r.URL.Path, "/debug/pprof/trace"):
pprof.Trace(w, r)
case strings.HasPrefix(r.URL.Path, "/debug/pprof"):
pprof.Index(w, r)
case strings.HasPrefix(r.URL.Path, "/api/auth"):
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/backup"):
@@ -181,8 +192,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/docker"):
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):

View File

@@ -2,6 +2,7 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@@ -107,7 +108,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = r.Host
hostURL = strings.Split(r.Host, ":")[0]
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -144,7 +145,8 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{

View File

@@ -7,7 +7,6 @@ import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/http/security"
)
@@ -25,14 +24,12 @@ type Handler struct {
JWTService dataservices.JWTService
LDAPService portainer.LDAPService
SnapshotService portainer.SnapshotService
demoService *demo.Service
}
// NewHandler creates a handler to manage settings operations.
func NewHandler(bouncer *security.RequestBouncer, demoService *demo.Service) *Handler {
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
demoService: demoService,
Router: mux.NewRouter(),
}
h.Handle("/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)

View File

@@ -14,8 +14,6 @@ type publicSettingsResponse struct {
LogoURL string `json:"LogoURL" example:"https://mycompany.mydomain.tld/logo.png"`
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
// The minimum required length for a password of any user when using internal auth mode
RequiredPasswordLength int `json:"RequiredPasswordLength" example:"1"`
// Whether edge compute features are enabled
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
// Supported feature flags
@@ -28,21 +26,6 @@ type publicSettingsResponse struct {
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
// The expiry of a Kubeconfig
KubeconfigExpiry string `example:"24h" default:"0"`
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
Edge struct {
// Whether the device has been started in edge async mode
AsyncMode bool
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
// The check in interval for edge agent (in seconds) - used in non async mode [seconds]
CheckinInterval int `example:"60"`
}
}
// @id SettingsPublic
@@ -68,19 +51,11 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
publicSettings := &publicSettingsResponse{
LogoURL: appSettings.LogoURL,
AuthenticationMethod: appSettings.AuthenticationMethod,
RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength,
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
EnableTelemetry: appSettings.EnableTelemetry,
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: appSettings.FeatureFlagSettings,
}
publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
@@ -94,11 +69,5 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
publicSettings.OAuthLoginURI += "&prompt=login"
}
}
//if LDAP authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
}
return publicSettings
}

View File

@@ -22,10 +22,9 @@ type settingsUpdatePayload struct {
// A list of label name & value that will be used to hide containers when querying containers
BlackListedLabels []portainer.Pair
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth
AuthenticationMethod *int `example:"1"`
InternalAuthSettings *portainer.InternalAuthSettings `example:""`
LDAPSettings *portainer.LDAPSettings `example:""`
OAuthSettings *portainer.OAuthSettings `example:""`
AuthenticationMethod *int `example:"1"`
LDAPSettings *portainer.LDAPSettings `example:""`
OAuthSettings *portainer.OAuthSettings `example:""`
// The interval in which environment(endpoint) snapshots are created
SnapshotInterval *string `example:"5m"`
// URL to the templates that will be displayed in the UI when navigating to App Templates
@@ -114,11 +113,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
}
if handler.demoService.IsDemo() {
payload.EnableTelemetry = nil
payload.LogoURL = nil
}
if payload.AuthenticationMethod != nil {
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
}
@@ -154,10 +148,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.BlackListedLabels = payload.BlackListedLabels
}
if payload.InternalAuthSettings != nil {
settings.InternalAuthSettings.RequiredPasswordLength = payload.InternalAuthSettings.RequiredPasswordLength
}
if payload.LDAPSettings != nil {
ldapReaderDN := settings.LDAPSettings.ReaderDN
ldapPassword := settings.LDAPSettings.Password

View File

@@ -177,6 +177,9 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}

View File

@@ -70,6 +70,9 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ManifestFile) {
return errors.New("Invalid manifest file in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}

View File

@@ -144,6 +144,9 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}

View File

@@ -7,8 +7,6 @@ import (
"strings"
"sync"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -23,6 +21,8 @@ import (
"github.com/portainer/portainer/api/stacks"
)
const defaultGitReferenceName = "refs/heads/master"
var (
errStackAlreadyExists = errors.New("A stack already exists with this name")
errWebhookIDAlreadyExists = errors.New("A webhook ID already exists")
@@ -135,20 +135,6 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
// if stack management is disabled for non admins and the user isn't an admin, then return false. Otherwise return true
func (handler *Handler) userCanManageStacks(securityContext *security.RestrictedRequestContext, endpoint *portainer.Endpoint) (bool, error) {
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpoint.ID))
if err != nil {
return false, fmt.Errorf("Failed to get user from the database: %w", err)
}
return canCreate, nil
}
return true, nil
}
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {

View File

@@ -82,22 +82,6 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
stack.EndpointID = portainer.EndpointID(endpointID)
stack.SwarmID = swarmId

View File

@@ -13,6 +13,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -75,18 +76,22 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
}
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
}
if !canCreate {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)

View File

@@ -103,15 +103,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack deletion is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)

View File

@@ -3,12 +3,11 @@ package stacks
import (
"net/http"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -60,15 +59,6 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -86,7 +76,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
}
}

View File

@@ -3,12 +3,12 @@ package stacks
import (
"net/http"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/http/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -55,15 +55,6 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -81,7 +72,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {

View File

@@ -87,15 +87,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack migration is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}

View File

@@ -64,15 +64,6 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}

View File

@@ -75,15 +75,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}

View File

@@ -123,15 +123,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
if updateError != nil {
return updateError

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -18,7 +19,6 @@ import (
type stackGitUpdatePayload struct {
AutoUpdate *portainer.StackAutoUpdate
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
@@ -26,6 +26,10 @@ type stackGitUpdatePayload struct {
}
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
@@ -120,15 +124,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
@@ -141,12 +136,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -24,10 +25,12 @@ type stackGitRedployPayload struct {
RepositoryUsername string
RepositoryPassword string
Env []portainer.Pair
Prune bool
}
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
@@ -111,15 +114,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
var payload stackGitRedployPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -128,11 +122,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.Env = payload.Env
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
@@ -202,11 +191,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch stack.Type {
case portainer.DockerSwarmStack:
prune := false
if stack.Option != nil {
prune = stack.Option.Prune
}
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, prune)
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}

View File

@@ -38,6 +38,9 @@ func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error
}
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}

View File

@@ -5,29 +5,26 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle status operations.
type Handler struct {
*mux.Router
Status *portainer.Status
demoService *demo.Service
Status *portainer.Status
}
// NewHandler creates a handler to manage status operations.
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demoService *demo.Service) *Handler {
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Handler {
h := &Handler{
Router: mux.NewRouter(),
Status: status,
demoService: demoService,
Router: mux.NewRouter(),
Status: status,
}
h.Handle("/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
h.Handle("/status/version",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
return h
}

View File

@@ -5,26 +5,16 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/demo"
)
type status struct {
*portainer.Status
DemoEnvironment demo.EnvironmentDetails
}
// @id StatusInspect
// @summary Check Portainer status
// @description Retrieve Portainer status
// @description **Access policy**: public
// @tags status
// @produce json
// @success 200 {object} status "Success"
// @success 200 {object} portainer.Status "Success"
// @router /status [get]
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
return response.JSON(w, &status{
Status: handler.Status,
DemoEnvironment: handler.demoService.Details(),
})
return response.JSON(w, handler.Status)
}

View File

@@ -0,0 +1,62 @@
package status
import (
"encoding/json"
"net/http"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type inspectVersionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
}
type githubData struct {
TagName string `json:"tag_name"`
}
// @id StatusInspectVersion
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} inspectVersionResponse "Success"
// @router /status/version [get]
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
var data githubData
err = json.Unmarshal(motd, &data)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
resp := inspectVersionResponse{
UpdateAvailable: false,
}
currentVersion := semver.New(portainer.APIVersion)
latestVersion := semver.New(data.TagName)
if currentVersion.LessThan(*latestVersion) {
resp.UpdateAvailable = true
resp.LatestVersion = data.TagName
}
response.JSON(w, &resp)
}

View File

@@ -1,105 +0,0 @@
package status
import (
"encoding/json"
"net/http"
"strconv"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
log "github.com/sirupsen/logrus"
)
type versionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
ServerVersion string
DatabaseVersion string
Build BuildInfo
}
type BuildInfo struct {
BuildNumber string
ImageTag string
NodejsVersion string
YarnVersion string
WebpackVersion string
GoVersion string
}
// @id Version
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} versionResponse "Success"
// @router /status/version [get]
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
result := &versionResponse{
ServerVersion: portainer.APIVersion,
DatabaseVersion: strconv.Itoa(portainer.DBVersion),
Build: BuildInfo{
BuildNumber: build.BuildNumber,
ImageTag: build.ImageTag,
NodejsVersion: build.NodejsVersion,
YarnVersion: build.YarnVersion,
WebpackVersion: build.WebpackVersion,
GoVersion: build.GoVersion,
},
}
latestVersion := getLatestVersion()
if hasNewerVersion(portainer.APIVersion, latestVersion) {
result.UpdateAvailable = true
result.LatestVersion = latestVersion
}
response.JSON(w, &result)
}
func getLatestVersion() string {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
log.WithError(err).Debug("couldn't fetch latest Portainer release version")
return ""
}
var data struct {
TagName string `json:"tag_name"`
}
err = json.Unmarshal(motd, &data)
if err != nil {
log.WithError(err).Debug("couldn't parse latest Portainer version")
return ""
}
return data.TagName
}
func hasNewerVersion(currentVersion, latestVersion string) bool {
currentVersionSemver, err := semver.NewVersion(currentVersion)
if err != nil {
log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver")
return false
}
latestVersionSemver, err := semver.NewVersion(latestVersion)
if err != nil {
log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver")
return false
}
return currentVersionSemver.LessThan(*latestVersionSemver)
}

View File

@@ -126,11 +126,12 @@ func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edg
}
endpointStacks := edge.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
updatedStacks := make(map[portainer.EdgeStackID]portainer.EdgeStackStatus)
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
updatedStacks[edgeStackID] = endpointRelation.EdgeStacks[edgeStackID]
}
endpointRelation.EdgeStacks = stacksSet
endpointRelation.EdgeStacks = updatedStacks
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
}

Some files were not shown because too many files have changed in this diff Show More