mirror of
https://github.com/portainer/portainer.git
synced 2025-08-10 08:15:25 +02:00
Merge branch 'develop' into feat-fdo-1.1
This commit is contained in:
commit
540afcd179
1711 changed files with 39687 additions and 27271 deletions
|
@ -31,7 +31,12 @@ rules:
|
|||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
|
||||
pathGroups:
|
||||
[
|
||||
{ pattern: '@@/**', group: 'internal', position: 'after' },
|
||||
{ pattern: '@/**', group: 'internal' },
|
||||
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
|
||||
],
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroupsExcludedImportTypes: ['internal'],
|
||||
},
|
||||
|
@ -41,6 +46,7 @@ settings:
|
|||
'import/resolver':
|
||||
alias:
|
||||
map:
|
||||
- ['@@', './app/react/components']
|
||||
- ['@', './app']
|
||||
extensions: ['.js', '.ts', '.tsx']
|
||||
|
||||
|
@ -52,6 +58,7 @@ overrides:
|
|||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
- 'regex'
|
||||
extends:
|
||||
- airbnb
|
||||
- airbnb-typescript
|
||||
|
@ -68,7 +75,14 @@ overrides:
|
|||
version: 'detect'
|
||||
rules:
|
||||
import/order:
|
||||
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
|
||||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { 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 }]
|
||||
|
@ -90,6 +104,7 @@ 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
1
.gitignore
vendored
|
@ -7,6 +7,7 @@ storybook-static
|
|||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
.vscode
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
|
|
|
@ -3,6 +3,7 @@ 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({
|
||||
|
@ -31,11 +32,17 @@ export const parameters = {
|
|||
},
|
||||
};
|
||||
|
||||
const testQueryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||
<Story />
|
||||
</UIRouter>
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||
<Story />
|
||||
</UIRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
|
|
@ -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.9.x**. Portainer is on version 2, the second number denotes the month of release.
|
||||
**The latest version of Portainer is 2.13.x**.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
76
api/agent/version.go
Normal file
76
api/agent/version.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
netUrl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// 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(url string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.Contains(url, "://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
parsedURL, err := netUrl.Parse(fmt.Sprintf("%s/ping", url))
|
||||
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
|
||||
}
|
9
api/build/variables.go
Normal file
9
api/build/variables.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
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
|
|
@ -35,6 +35,7 @@ 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(),
|
||||
|
|
|
@ -16,6 +16,7 @@ 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"
|
||||
|
@ -23,6 +24,7 @@ import (
|
|||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
@ -572,6 +574,7 @@ 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)
|
||||
|
@ -607,7 +610,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)
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
|
@ -634,6 +637,14 @@ 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)
|
||||
|
@ -722,6 +733,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -732,7 +744,15 @@ func main() {
|
|||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
||||
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")
|
||||
err := server.Start()
|
||||
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
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}`
|
||||
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}`
|
||||
passphrase = "my secret key"
|
||||
)
|
||||
|
||||
|
|
|
@ -103,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
|||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
dbPath := store.databasePath()
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
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
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
|
|
|
@ -47,6 +47,9 @@ 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,
|
||||
|
|
|
@ -34,9 +34,9 @@ func TestMigrateData(t *testing.T) {
|
|||
wantPath string
|
||||
}{
|
||||
{
|
||||
testName: "migrate version 24 to 35",
|
||||
testName: "migrate version 24 to latest",
|
||||
srcPath: "test_data/input_24.json",
|
||||
wantPath: "test_data/output_35.json",
|
||||
wantPath: "test_data/output_24_to_latest.json",
|
||||
},
|
||||
}
|
||||
for _, test := range snapshotTests {
|
||||
|
|
|
@ -100,6 +100,12 @@ 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
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
@ -210,14 +211,14 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
|||
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)
|
||||
volumesData := snapshot.SnapshotRaw.Volumes
|
||||
if volumesData.Volumes == nil {
|
||||
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
|
||||
continue
|
||||
}
|
||||
|
||||
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
|
||||
|
||||
}
|
||||
|
||||
for _, resourceControl := range volumeResourceControls {
|
||||
|
@ -240,18 +241,11 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
|
20
api/datastore/migrator/migrate_dbversion50.go
Normal file
20
api/datastore/migrator/migrate_dbversion50.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
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)
|
||||
}
|
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
}
|
|
@ -27,6 +27,9 @@
|
|||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"Agent": {
|
||||
"Version": ""
|
||||
},
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"AzureCredentials": {
|
||||
|
@ -35,8 +38,15 @@
|
|||
"TenantID": ""
|
||||
},
|
||||
"ComposeSyntaxMaxVersion": "",
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeCheckinInterval": 0,
|
||||
"EdgeKey": "",
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
|
@ -70,12 +80,107 @@
|
|||
"DockerSnapshotRaw": {
|
||||
"Containers": null,
|
||||
"Images": null,
|
||||
"Info": 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
|
||||
},
|
||||
"Networks": null,
|
||||
"Version": null,
|
||||
"Volumes": null
|
||||
"Version": {
|
||||
"ApiVersion": "",
|
||||
"Arch": "",
|
||||
"GitCommit": "",
|
||||
"GoVersion": "",
|
||||
"Os": "",
|
||||
"Platform": {
|
||||
"Name": ""
|
||||
},
|
||||
"Version": ""
|
||||
},
|
||||
"Volumes": {
|
||||
"Volumes": null,
|
||||
"Warnings": null
|
||||
}
|
||||
},
|
||||
"DockerVersion": "20.10.13",
|
||||
"GpuUseAll": false,
|
||||
"GpuUseList": null,
|
||||
"HealthyContainerCount": 0,
|
||||
"ImageCount": 9,
|
||||
"NodeCount": 0,
|
||||
|
@ -589,6 +694,12 @@
|
|||
"BlackListedLabels": [],
|
||||
"DisplayDonationHeader": false,
|
||||
"DisplayExternalContributors": false,
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeAgentCheckinInterval": 5,
|
||||
"EdgePortainerUrl": "",
|
||||
"EnableEdgeComputeFeatures": false,
|
||||
|
@ -597,6 +708,9 @@
|
|||
"EnforceEdgeID": false,
|
||||
"FeatureFlagSettings": null,
|
||||
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
||||
"InternalAuthSettings": {
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell",
|
||||
"LDAPSettings": {
|
||||
|
@ -682,6 +796,7 @@
|
|||
"IsComposeFormat": false,
|
||||
"Name": "alpine",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
|
@ -704,6 +819,7 @@
|
|||
"IsComposeFormat": false,
|
||||
"Name": "redis",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
|
@ -726,6 +842,7 @@
|
|||
"IsComposeFormat": false,
|
||||
"Name": "nginx",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
|
@ -802,7 +919,7 @@
|
|||
],
|
||||
"version": {
|
||||
"DB_UPDATING": "false",
|
||||
"DB_VERSION": "35",
|
||||
"DB_VERSION": "60",
|
||||
"INSTANCE_ID": "null"
|
||||
}
|
||||
}
|
118
api/demo/demo.go
Normal file
118
api/demo/demo.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
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
|
||||
}
|
79
api/demo/init.go
Normal file
79
api/demo/init.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
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")
|
||||
}
|
|
@ -7,9 +7,10 @@ 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"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||
|
@ -154,11 +155,35 @@ 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)") {
|
||||
|
@ -174,6 +199,14 @@ 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
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -14,7 +13,6 @@ 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"
|
||||
|
@ -56,13 +54,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
envFile, 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, envFilePath, forceRereate)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
|
||||
|
@ -76,12 +74,14 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
if err := updateNetworkEnvFile(stack); err != nil {
|
||||
return err
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
|
@ -103,200 +103,42 @@ 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
|
||||
}
|
||||
|
||||
func fileNotExist(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
// 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 {
|
||||
return errors.Wrap(err, "failed to open env file")
|
||||
// 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
|
||||
}
|
||||
|
||||
defer envfile.Close()
|
||||
defer defaultEnvFile.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
|
||||
if _, err = io.Copy(w, defaultEnvFile); err == nil {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
|
||||
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
|
||||
// If couldn't copy the .env file, then ignore the error and try to continue
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
const composeFile = `version: "3.9"
|
||||
|
@ -41,6 +42,8 @@ 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)
|
||||
|
|
|
@ -65,56 +65,22 @@ func Test_createEnvFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_createNetworkEnvFile(t *testing.T) {
|
||||
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
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{
|
||||
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
|
||||
stack := &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: "test", Value: "test-value"},
|
||||
{Name: "VAR1", Value: "NEW_VAL1"},
|
||||
{Name: "VAR3", Value: "VAL3"},
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
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))
|
||||
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
|
||||
}
|
||||
|
|
|
@ -108,12 +108,12 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
|
|||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
|
||||
rootItemUrl, err := a.buildRootItemUrl(config, options.referenceName)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build azure refs url")
|
||||
return "", errors.WithMessage(err, "failed to build azure root item url")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", refsUrl, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
|
||||
if options.username != "" || options.password != "" {
|
||||
req.SetBasicAuth(options.username, options.password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
|
@ -131,26 +131,24 @@ 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 refs with a status \"%v\"", resp.Status)
|
||||
return "", fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status)
|
||||
}
|
||||
|
||||
var refs struct {
|
||||
var items struct {
|
||||
Value []struct {
|
||||
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
|
||||
CommitId string `json:"commitId"`
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
|
||||
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
|
||||
}
|
||||
|
||||
func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||
|
@ -236,8 +234,10 @@ 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")
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
if 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) buildRefsUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/refs",
|
||||
func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
|
||||
a.baseUrl,
|
||||
url.PathEscape(config.organisation),
|
||||
url.PathEscape(config.project),
|
||||
|
@ -255,12 +255,15 @@ func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName strin
|
|||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
|
||||
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
|
||||
}
|
||||
|
||||
// filterContains=main&api-version=6.0
|
||||
q := u.Query()
|
||||
q.Set("filterContains", formatReferenceName(referenceName))
|
||||
q.Set("scopePath", "/")
|
||||
if referenceName != "" {
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
}
|
||||
q.Set("api-version", "6.0")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
|
|
|
@ -28,15 +28,15 @@ func Test_buildDownloadUrl(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_buildRefsUrl(t *testing.T) {
|
||||
func Test_buildRootItemUrl(t *testing.T) {
|
||||
a := NewAzureDownloader(nil)
|
||||
u, err := a.buildRefsUrl(&azureOptions{
|
||||
u, err := a.buildRootItemUrl(&azureOptions{
|
||||
organisation: "organisation",
|
||||
project: "project",
|
||||
repository: "repository",
|
||||
}, "refs/heads/main")
|
||||
|
||||
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/refs?filterContains=main&api-version=6.0")
|
||||
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")
|
||||
actualUrl, _ := url.Parse(u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
|
||||
|
@ -270,63 +270,17 @@ 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 := `{
|
||||
"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
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}`
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(response))
|
||||
|
@ -347,19 +301,11 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
|||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOptions{
|
||||
referenceName: "refs/heads/master",
|
||||
referenceName: "",
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
|
||||
want: "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
want: "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
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 {
|
||||
|
|
|
@ -82,8 +82,17 @@ 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(), opt.referenceName) {
|
||||
if strings.EqualFold(ref.Name().String(), referenceName) {
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
}
|
||||
|
|
18
api/go.mod
18
api/go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/portainer/portainer/api
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
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.9+incompatible
|
||||
github.com/docker/docker v20.10.16+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.6
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
|
@ -32,8 +32,8 @@ 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-20220407011010-3c7408969ad3
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
|
||||
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
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
|
@ -43,6 +43,7 @@ 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
|
||||
|
@ -61,7 +62,6 @@ 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,6 +95,9 @@ 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
|
||||
|
@ -112,12 +115,11 @@ 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
|
||||
|
|
840
api/go.sum
840
api/go.sum
File diff suppressed because it is too large
Load diff
|
@ -9,4 +9,6 @@ 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")
|
||||
)
|
||||
|
|
|
@ -13,7 +13,6 @@ 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 {
|
||||
|
@ -101,7 +100,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
|
|||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
forceChangePassword := !passwordutils.StrengthCheck(password)
|
||||
forceChangePassword := !handler.passwordStrengthChecker.Check(password)
|
||||
return handler.writeToken(w, user, forceChangePassword)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,14 @@ 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) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
|
|
|
@ -18,6 +18,7 @@ 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"
|
||||
|
@ -49,7 +50,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).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
@ -86,7 +87,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).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
|
|
@ -9,6 +9,8 @@ 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"
|
||||
)
|
||||
|
@ -25,7 +27,17 @@ 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) *Handler {
|
||||
func NewHandler(
|
||||
bouncer *security.RequestBouncer,
|
||||
dataStore dataservices.DataStore,
|
||||
gate *offlinegate.OfflineGate,
|
||||
filestorePath string,
|
||||
shutdownTrigger context.CancelFunc,
|
||||
adminMonitor *adminmonitor.Monitor,
|
||||
demoService *demo.Service,
|
||||
|
||||
) *Handler {
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
|
@ -36,8 +48,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
|||
adminMonitor: adminMonitor,
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
return h
|
||||
}
|
||||
|
@ -50,7 +65,7 @@ func adminAccess(next http.Handler) http.Handler {
|
|||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
|
|
@ -14,6 +14,7 @@ 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"
|
||||
|
@ -51,7 +52,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)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, test.backupPassword)
|
||||
|
@ -74,7 +75,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)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, "password")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package customtemplates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -115,6 +116,8 @@ 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 {
|
||||
|
@ -136,6 +139,12 @@ 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
|
||||
}
|
||||
|
||||
|
@ -164,6 +173,7 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p
|
|||
Platform: (payload.Platform),
|
||||
Type: (payload.Type),
|
||||
Logo: payload.Logo,
|
||||
Variables: payload.Variables,
|
||||
}
|
||||
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
|
@ -204,6 +214,8 @@ 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 {
|
||||
|
@ -236,6 +248,12 @@ 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
|
||||
}
|
||||
|
||||
|
@ -256,6 +274,7 @@ 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))
|
||||
|
@ -316,6 +335,8 @@ 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 {
|
||||
|
@ -361,6 +382,17 @@ 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
|
||||
}
|
||||
|
||||
|
@ -381,6 +413,7 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
|
|||
Type: payload.Type,
|
||||
Logo: payload.Logo,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Variables: payload.Variables,
|
||||
}
|
||||
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
|
|
|
@ -31,6 +31,8 @@ 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 {
|
||||
|
@ -52,6 +54,12 @@ 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
|
||||
}
|
||||
|
||||
|
@ -124,6 +132,7 @@ 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 {
|
||||
|
|
19
api/http/handler/customtemplates/utils.go
Normal file
19
api/http/handler/customtemplates/utils.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
}
|
86
api/http/handler/docker/containers/container_gpus_inspect.go
Normal file
86
api/http/handler/docker/containers/container_gpus_inspect.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
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})
|
||||
}
|
31
api/http/handler/docker/containers/handler.go
Normal file
31
api/http/handler/docker/containers/handler.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
}
|
63
api/http/handler/docker/handler.go
Normal file
63
api/http/handler/docker/handler.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
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)
|
||||
})
|
||||
}
|
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
|
@ -0,0 +1,924 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -77,14 +77,17 @@ 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)
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
}
|
||||
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
|
||||
version := r.Header.Get(portainer.PortainerAgentHeader)
|
||||
endpoint.Agent.Version = version
|
||||
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
|
|
|
@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
|
|||
portainer.EndpointRelation{
|
||||
EndpointID: 2,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
|
@ -194,7 +194,9 @@ func TestWithEndpoints(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
@ -239,6 +241,7 @@ 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)
|
||||
|
@ -355,6 +358,7 @@ 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)
|
||||
|
@ -424,6 +428,7 @@ 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)
|
||||
|
|
50
api/http/handler/endpoints/endpoint_agent_versions.go
Normal file
50
api/http/handler/endpoints/endpoint_agent_versions.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
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())
|
||||
}
|
|
@ -23,7 +23,7 @@ import (
|
|||
// @tags endpoints
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 {object} portainer.Endpoint "Success"
|
||||
// @success 204 "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.JSON(w, endpoint)
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeKey(edgeKey string) (string, error) {
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
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"
|
||||
|
@ -25,6 +24,7 @@ type endpointCreatePayload struct {
|
|||
URL string
|
||||
EndpointCreationType endpointCreationEnum
|
||||
PublicURL string
|
||||
Gpus []portainer.Pair
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
|
@ -142,6 +142,13 @@ 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
|
||||
|
||||
|
@ -187,6 +194,15 @@ 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
|
||||
|
@ -228,6 +244,7 @@ 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)
|
||||
|
@ -240,12 +257,25 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
|||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
var agentVersion string
|
||||
if payload.EndpointCreationType == agentEnvironment {
|
||||
agentPlatform, err := handler.pingAndCheckPlatform(payload)
|
||||
|
||||
payload.URL = "tcp://" + normalizeAgentAddress(payload.URL)
|
||||
|
||||
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)
|
||||
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 {
|
||||
|
@ -255,7 +285,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
|||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType)
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
|
@ -281,6 +311,7 @@ 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,
|
||||
|
@ -314,6 +345,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
|||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
|
@ -369,6 +401,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
|||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
|
@ -403,6 +436,7 @@ 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,
|
||||
|
@ -423,7 +457,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
|||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
|
@ -432,6 +466,7 @@ 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,
|
||||
|
@ -445,6 +480,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
|||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
||||
err := handler.storeTLSFiles(endpoint, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -538,58 +575,3 @@ 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
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ 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)
|
||||
|
|
|
@ -8,6 +8,7 @@ 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
|
||||
|
@ -29,6 +30,10 @@ 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}
|
||||
|
|
|
@ -4,24 +4,14 @@ 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 (
|
||||
|
@ -29,12 +19,10 @@ 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 account otherwise it will
|
||||
// @description return all environments(endpoints) if using an administrator or team leader account otherwise it will
|
||||
// @description only return authorized environments(endpoints).
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoints
|
||||
|
@ -42,14 +30,21 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
|
|||
// @security jwt
|
||||
// @produce json
|
||||
// @param start query int false "Start searching from"
|
||||
// @param search query string false "Search query"
|
||||
// @param groupId query int false "List environments(endpoints) of this group"
|
||||
// @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 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 edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
|
||||
// @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"
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints [get]
|
||||
|
@ -59,105 +54,43 @@ 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.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
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
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)
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to filter endpoints", err)
|
||||
}
|
||||
|
||||
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")
|
||||
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
|
||||
|
||||
filteredEndpointCount := len(filteredEndpoints)
|
||||
|
||||
|
@ -196,65 +129,7 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
|||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
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) {
|
||||
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
|
||||
|
||||
switch sortField {
|
||||
case "Name":
|
||||
|
@ -265,10 +140,20 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, sortField string, isSo
|
|||
}
|
||||
|
||||
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(endpoints)))
|
||||
sort.Stable(sort.Reverse(endpointsByGroup))
|
||||
} else {
|
||||
sort.Stable(EndpointsByGroup(endpoints))
|
||||
sort.Stable(endpointsByGroup)
|
||||
}
|
||||
|
||||
case "Status":
|
||||
|
@ -284,123 +169,6 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, sortField string, isSo
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -411,57 +179,3 @@ 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
|
||||
|
||||
}
|
||||
|
|
|
@ -16,66 +16,147 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
type endpointListTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
filter string
|
||||
}
|
||||
|
||||
func Test_endpointList(t *testing.T) {
|
||||
var err error
|
||||
is := assert.New(t)
|
||||
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,
|
||||
})
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEndpoint,
|
||||
untrustedEndpoint,
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
trustedEdgeDevice,
|
||||
untrustedEdgeDevice,
|
||||
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{
|
||||
{
|
||||
"should show all edge endpoints",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterAll,
|
||||
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 only trusted edge devices",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterTrusted,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only trusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
},
|
||||
{
|
||||
"should show only untrusted edge devices",
|
||||
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterUntrusted,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only untrusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
edgeDeviceUntrusted: true,
|
||||
},
|
||||
{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterNone,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(false),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -83,8 +164,13 @@ func Test_endpointList(t *testing.T) {
|
|||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
req := buildEndpointListRequest(test.filter)
|
||||
resp, err := doEndpointListRequest(req, h, is)
|
||||
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)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
|
@ -100,8 +186,28 @@ func Test_endpointList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(filter string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
|
||||
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)
|
||||
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
|
|
|
@ -55,6 +55,7 @@ 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 {
|
||||
|
|
|
@ -47,6 +47,7 @@ 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 {
|
||||
|
|
|
@ -22,6 +22,8 @@ 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)
|
||||
|
@ -88,17 +90,37 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
endpoint.Name = *payload.Name
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
if payload.URL != nil {
|
||||
endpoint.URL = *payload.URL
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
endpoint.URL = normalizeAgentAddress(*payload.URL)
|
||||
} else {
|
||||
endpoint.URL = *payload.URL
|
||||
}
|
||||
}
|
||||
|
||||
if payload.PublicURL != nil {
|
||||
endpoint.PublicURL = *payload.PublicURL
|
||||
}
|
||||
|
||||
if payload.Gpus != nil {
|
||||
endpoint.Gpus = payload.Gpus
|
||||
}
|
||||
|
||||
if payload.EdgeCheckinInterval != nil {
|
||||
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
||||
}
|
||||
|
@ -254,7 +276,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
}
|
||||
|
||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||
if (payload.URL != nil && *payload.URL != endpoint.URL) || (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) || endpoint.Type == portainer.AzureEnvironment {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
|
||||
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
|
|
435
api/http/handler/endpoints/filter.go
Normal file
435
api/http/handler/endpoints/filter.go
Normal file
|
@ -0,0 +1,435 @@
|
|||
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
|
||||
}
|
177
api/http/handler/endpoints/filter_test.go
Normal file
177
api/http/handler/endpoints/filter_test.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
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
|
||||
}
|
|
@ -4,6 +4,7 @@ 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"
|
||||
|
@ -35,6 +36,7 @@ type requestBouncer interface {
|
|||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer requestBouncer
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
|
@ -48,10 +50,11 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer requestBouncer) *Handler {
|
||||
func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
demoService: demoService,
|
||||
}
|
||||
|
||||
h.Handle("/endpoints",
|
||||
|
@ -64,6 +67,9 @@ func NewHandler(bouncer requestBouncer) *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}",
|
||||
|
|
|
@ -21,23 +21,26 @@ func (e EndpointsByName) Less(i, j int) bool {
|
|||
return sortorder.NaturalLess(strings.ToLower(e[i].Name), strings.ToLower(e[j].Name))
|
||||
}
|
||||
|
||||
type EndpointsByGroup []portainer.Endpoint
|
||||
type EndpointsByGroup struct {
|
||||
endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
endpoints []portainer.Endpoint
|
||||
}
|
||||
|
||||
func (e EndpointsByGroup) Len() int {
|
||||
return len(e)
|
||||
return len(e.endpoints)
|
||||
}
|
||||
|
||||
func (e EndpointsByGroup) Swap(i, j int) {
|
||||
e[i], e[j] = e[j], e[i]
|
||||
e.endpoints[i], e.endpoints[j] = e.endpoints[j], e.endpoints[i]
|
||||
}
|
||||
|
||||
func (e EndpointsByGroup) Less(i, j int) bool {
|
||||
if e[i].GroupID == e[j].GroupID {
|
||||
if e.endpoints[i].GroupID == e.endpoints[j].GroupID {
|
||||
return false
|
||||
}
|
||||
|
||||
groupA := endpointGroupNames[e[i].GroupID]
|
||||
groupB := endpointGroupNames[e[j].GroupID]
|
||||
groupA := e.endpointGroupNames[e.endpoints[i].GroupID]
|
||||
groupB := e.endpointGroupNames[e.endpoints[j].GroupID]
|
||||
|
||||
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
|
||||
}
|
||||
|
|
18
api/http/handler/endpoints/unique_name.go
Normal file
18
api/http/handler/endpoints/unique_name.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
}
|
18
api/http/handler/endpoints/utils.go
Normal file
18
api/http/handler/endpoints/utils.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package endpoints
|
||||
|
||||
import "strings"
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
}
|
||||
|
||||
func normalizeAgentAddress(url string) string {
|
||||
// Case insensitive strip http or https scheme if URL entered
|
||||
index := strings.Index(url, "://")
|
||||
if index >= 0 {
|
||||
return url[index+3:]
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"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"
|
||||
|
@ -45,6 +46,7 @@ type Handler struct {
|
|||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHandler *docker.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
|
@ -80,7 +82,7 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.13.0
|
||||
// @version 2.15.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package helm
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
|
@ -108,7 +107,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
|
|||
|
||||
hostURL := "localhost"
|
||||
if !sslSettings.SelfSigned {
|
||||
hostURL = strings.Split(r.Host, ":")[0]
|
||||
hostURL = r.Host
|
||||
}
|
||||
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -145,8 +144,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
|||
}
|
||||
|
||||
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
hostURL := strings.Split(r.Host, ":")[0]
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
|
||||
return clientV1.NamedCluster{
|
||||
Name: buildClusterName(endpoint.Name),
|
||||
Cluster: clientV1.Cluster{
|
||||
|
|
|
@ -7,6 +7,7 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -24,12 +25,14 @@ 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) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
|
|
|
@ -14,6 +14,8 @@ 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
|
||||
|
@ -26,6 +28,21 @@ 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
|
||||
|
@ -51,11 +68,19 @@ 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
|
||||
|
@ -69,5 +94,11 @@ 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
|
||||
}
|
||||
|
|
|
@ -22,9 +22,10 @@ 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"`
|
||||
LDAPSettings *portainer.LDAPSettings `example:""`
|
||||
OAuthSettings *portainer.OAuthSettings `example:""`
|
||||
AuthenticationMethod *int `example:"1"`
|
||||
InternalAuthSettings *portainer.InternalAuthSettings `example:""`
|
||||
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
|
||||
|
@ -113,6 +114,11 @@ 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)
|
||||
}
|
||||
|
@ -148,6 +154,10 @@ 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
|
||||
|
|
|
@ -177,9 +177,6 @@ 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")
|
||||
}
|
||||
|
|
|
@ -70,9 +70,6 @@ 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
|
||||
}
|
||||
|
|
|
@ -144,9 +144,6 @@ 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")
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -21,8 +23,6 @@ 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,6 +135,20 @@ 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 {
|
||||
|
|
|
@ -82,6 +82,22 @@ 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
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -76,22 +75,18 @@ 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}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
||||
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)}
|
||||
}
|
||||
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)}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
|
|
|
@ -103,6 +103,15 @@ 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)
|
||||
|
|
|
@ -3,11 +3,12 @@ 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"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
@ -59,6 +60,15 @@ 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 {
|
||||
|
@ -76,7 +86,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", errors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@ package stacks
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
|
||||
"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/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
@ -55,6 +55,15 @@ 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 {
|
||||
|
@ -72,7 +81,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", errors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
|
|
|
@ -87,6 +87,15 @@ 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}
|
||||
|
|
|
@ -64,6 +64,15 @@ 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}
|
||||
|
|
|
@ -75,6 +75,15 @@ 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")}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,15 @@ 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
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -19,6 +18,7 @@ import (
|
|||
type stackGitUpdatePayload struct {
|
||||
AutoUpdate *portainer.StackAutoUpdate
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
|
@ -26,10 +26,6 @@ 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
|
||||
}
|
||||
|
@ -124,6 +120,15 @@ 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)
|
||||
|
@ -136,6 +141,12 @@ 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 {
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -25,12 +24,10 @@ 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
|
||||
}
|
||||
|
||||
|
@ -114,6 +111,15 @@ 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 {
|
||||
|
@ -122,6 +128,11 @@ 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)
|
||||
|
@ -191,7 +202,11 @@ 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:
|
||||
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
prune := false
|
||||
if stack.Option != nil {
|
||||
prune = stack.Option.Prune
|
||||
}
|
||||
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, prune)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
|
|
@ -38,9 +38,6 @@ 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
|
||||
}
|
||||
|
|
|
@ -5,26 +5,29 @@ import (
|
|||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"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
|
||||
Status *portainer.Status
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -5,16 +5,26 @@ 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} portainer.Status "Success"
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /status [get]
|
||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, handler.Status)
|
||||
return response.JSON(w, &status{
|
||||
Status: handler.Status,
|
||||
DemoEnvironment: handler.demoService.Details(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
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)
|
||||
}
|
105
api/http/handler/status/version.go
Normal file
105
api/http/handler/status/version.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
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)
|
||||
}
|
|
@ -21,14 +21,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||
|
||||
h.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipCreate)).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipList)).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipUpdate)).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipDelete)).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id TeamMembershipList
|
||||
|
@ -23,15 +21,6 @@ import (
|
|||
// @failure 500 "Server error"
|
||||
// @router /team_memberships [get]
|
||||
func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
memberships, err := handler.DataStore.TeamMembership().TeamMemberships()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err}
|
||||
|
|
|
@ -36,8 +36,8 @@ func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error {
|
|||
|
||||
// @id TeamMembershipUpdate
|
||||
// @summary Update a team membership
|
||||
// @description Update a team membership. Access is only available to administrators leaders of the associated team.
|
||||
// @description **Access policy**: administrator
|
||||
// @description Update a team membership. Access is only available to administrators or leaders of the associated team.
|
||||
// @description **Access policy**: administrator or leaders of the associated team
|
||||
// @tags team_memberships
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
|
@ -63,15 +63,6 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
|
@ -79,8 +70,15 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && membership.Role != portainer.MembershipRole(payload.Role) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", httperrors.ErrResourceAccessDenied}
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
isLeadingBothTeam := security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) &&
|
||||
security.AuthorizedTeamManagement(membership.TeamID, securityContext)
|
||||
if !(securityContext.IsAdmin || isLeadingBothTeam) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership.UserID = portainer.UserID(payload.UserID)
|
||||
|
|
|
@ -20,18 +20,22 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/teams/{id}/memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
||||
|
||||
adminRouter := h.NewRoute().Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
|
||||
restrictedRouter := h.NewRoute().Subrouter()
|
||||
restrictedRouter.Use(bouncer.RestrictedAccess)
|
||||
|
||||
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
|
||||
restrictedRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
|
||||
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
|
||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
|
||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamDelete)).Methods(http.MethodDelete)
|
||||
teamLeaderRouter.Handle("/teams/{id}/memberships", httperror.LoggerHandler(h.teamMemberships)).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
type teamCreatePayload struct {
|
||||
// Name
|
||||
Name string `example:"developers" validate:"required"`
|
||||
// TeamLeaders
|
||||
TeamLeaders []portainer.UserID `example:"3,5"`
|
||||
}
|
||||
|
||||
func (payload *teamCreatePayload) Validate(r *http.Request) error {
|
||||
|
@ -62,5 +64,18 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the team inside the database", err}
|
||||
}
|
||||
|
||||
for _, teamLeader := range payload.TeamLeaders {
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: teamLeader,
|
||||
TeamID: team.ID,
|
||||
Role: portainer.TeamLeader,
|
||||
}
|
||||
|
||||
err = handler.DataStore.TeamMembership().Create(membership)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team leadership inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, team)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type adminInitPayload struct {
|
||||
|
@ -58,7 +57,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusConflict, "Unable to create administrator user", errAdminAlreadyInitialized}
|
||||
}
|
||||
|
||||
if !passwordutils.StrengthCheck(payload.Password) {
|
||||
if !handler.passwordStrengthChecker.Check(payload.Password) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"net/http"
|
||||
|
@ -30,43 +31,51 @@ func hideFields(user *portainer.User) {
|
|||
// Handler is the HTTP handler used to handle user operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
bouncer *security.RequestBouncer
|
||||
apiKeyService apikey.APIKeyService
|
||||
DataStore dataservices.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
bouncer *security.RequestBouncer
|
||||
apiKeyService apikey.APIKeyService
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
passwordStrengthChecker security.PasswordStrengthChecker
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService, demoService *demo.Service, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
apiKeyService: apiKeyService,
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
apiKeyService: apiKeyService,
|
||||
demoService: demoService,
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/tokens",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userGetAccessTokens))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/tokens",
|
||||
rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreateAccessToken)))).Methods(http.MethodPost)
|
||||
h.Handle("/users/{id}/tokens/{keyID}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userRemoveAccessToken))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminInit))).Methods(http.MethodPost)
|
||||
|
||||
adminRouter := h.NewRoute().Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
|
||||
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
restrictedRouter := h.NewRoute().Subrouter()
|
||||
restrictedRouter.Use(bouncer.RestrictedAccess)
|
||||
|
||||
authenticatedRouter := h.NewRoute().Subrouter()
|
||||
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
publicRouter := h.NewRoute().Subrouter()
|
||||
publicRouter.Use(bouncer.PublicAccess)
|
||||
|
||||
adminRouter.Handle("/users", httperror.LoggerHandler(h.userCreate)).Methods(http.MethodPost)
|
||||
restrictedRouter.Handle("/users", httperror.LoggerHandler(h.userList)).Methods(http.MethodGet)
|
||||
restrictedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userInspect)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userUpdate)).Methods(http.MethodPut)
|
||||
adminRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userDelete)).Methods(http.MethodDelete)
|
||||
restrictedRouter.Handle("/users/{id}/tokens", httperror.LoggerHandler(h.userGetAccessTokens)).Methods(http.MethodGet)
|
||||
restrictedRouter.Handle("/users/{id}/tokens", rateLimiter.LimitAccess(httperror.LoggerHandler(h.userCreateAccessToken))).Methods(http.MethodPost)
|
||||
restrictedRouter.Handle("/users/{id}/tokens/{keyID}", httperror.LoggerHandler(h.userRemoveAccessToken)).Methods(http.MethodDelete)
|
||||
restrictedRouter.Handle("/users/{id}/memberships", httperror.LoggerHandler(h.userMemberships)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/users/{id}/passwd", rateLimiter.LimitAccess(httperror.LoggerHandler(h.userUpdatePassword))).Methods(http.MethodPut)
|
||||
publicRouter.Handle("/users/admin/check", httperror.LoggerHandler(h.adminCheck)).Methods(http.MethodGet)
|
||||
publicRouter.Handle("/users/admin/init", httperror.LoggerHandler(h.adminInit)).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -9,9 +9,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"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type userCreatePayload struct {
|
||||
|
@ -35,8 +32,7 @@ func (payload *userCreatePayload) Validate(r *http.Request) error {
|
|||
// @id UserCreate
|
||||
// @summary Create a new user
|
||||
// @description Create a new Portainer user.
|
||||
// @description Only team leaders and administrators can create users.
|
||||
// @description Only administrators can create an administrator user account.
|
||||
// @description Only administrators can create users.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags users
|
||||
// @security ApiKeyAuth
|
||||
|
@ -57,19 +53,6 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create user", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && payload.Role == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
|
@ -95,7 +78,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
if !passwordutils.StrengthCheck(payload.Password) {
|
||||
if !handler.passwordStrengthChecker.Check(payload.Password) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,8 +39,9 @@ func Test_userCreateAccessToken(t *testing.T) {
|
|||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
passwordChecker := security.NewPasswordStrengthChecker(store.SettingsService)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil, passwordChecker)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -31,8 +31,9 @@ func Test_deleteUserRemovesAccessTokens(t *testing.T) {
|
|||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
passwordChecker := security.NewPasswordStrengthChecker(store.SettingsService)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil, passwordChecker)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
|
|
@ -38,8 +38,9 @@ func Test_userGetAccessTokens(t *testing.T) {
|
|||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
passwordChecker := security.NewPasswordStrengthChecker(store.SettingsService)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil, passwordChecker)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -36,8 +36,9 @@ func Test_userRemoveAccessToken(t *testing.T) {
|
|||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
passwordChecker := security.NewPasswordStrengthChecker(store.SettingsService)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil, passwordChecker)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -57,6 +57,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
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/passwordutils"
|
||||
)
|
||||
|
||||
type userUpdatePasswordPayload struct {
|
||||
|
@ -55,6 +54,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
@ -79,10 +82,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
|||
|
||||
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", httperrors.ErrUnauthorized}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Current password doesn't match", errors.New("Current password does not match the password provided. Please try again")}
|
||||
}
|
||||
|
||||
if !passwordutils.StrengthCheck(payload.NewPassword) {
|
||||
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Password does not meet the requirements", nil}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,8 +31,9 @@ func Test_updateUserRemovesAccessTokens(t *testing.T) {
|
|||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
passwordChecker := security.NewPasswordStrengthChecker(store.SettingsService)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil, passwordChecker)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue