Compare commits

..

52 Commits

Author SHA1 Message Date
hookenz
7813ac87cf correctly detect unreachable 2024-05-09 15:29:27 +12:00
Oscar Zhou
24e0318280 fix(api): list docker volume performance [EE-6896] (#11754) 2024-05-09 13:02:42 +12:00
Matt Hook
9a079a83fa fix(pending-action): pending action data format [EE-7064] (#11793)
Co-authored-by: Prabhat Khera <91852476+prabhat-portainer@users.noreply.github.com>
2024-05-09 08:15:33 +12:00
Ali
1df6087c8e fix(auth logs): fix typo in search keyword [EE-6742] (#11791)
Co-authored-by: testa113 <testa113>
2024-05-08 09:16:02 +12:00
Ali
ae705bc245 fix(be-overlay): consistency overlay with variants [EE-6742] (#11775)
Co-authored-by: testa113 <testa113>
2024-05-07 16:16:52 +12:00
Ali
d725b5e3b6 fix(app): show one tooltip to describe rollback feature [EE-6825] (#11778)
Co-authored-by: testa113 <testa113>
2024-05-07 15:27:25 +12:00
cmeng
1b33b1f5dd fix(container): specify node name when get a container EE-6981 (#11750) 2024-05-07 11:34:37 +12:00
Steven Kang
b70f0fe3d2 fix: windows container capability [EE-5814] (#11765) 2024-05-03 10:56:38 +12:00
Ali
55ef46edb6 fix(namespace): wait for system ns setting to load before selecting existing ns [EE-6917] (#11709)
Co-authored-by: testa113 <testa113>
2024-05-02 16:43:08 +12:00
Prabhat Khera
c2654d55b3 fix(images): consider stopped containers for unused label [EE-6983] (#11630) 2024-05-02 14:35:28 +12:00
Prabhat Khera
7fab352dbf chore(version-bump): bump version to 2.20.3 [EE-7063] (#11756) 2024-05-02 14:33:41 +12:00
Matt Hook
0dcb5113f7 fix(kube): correctly extract namespace from namespace manifest [EE-6555] (#11674)
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2024-05-02 14:28:01 +12:00
Ali
a1b0634d86 fix(kube): fix text in activity and authentication logs teasers [EE-6742] (#11746)
Co-authored-by: testa113 <testa113>
2024-05-02 14:23:47 +12:00
Ali
da134c3e3f fix(app): avoid 'no label' error when deleting external app [EE-6019] (#11697)
Co-authored-by: testa113 <testa113>
2024-05-02 14:22:12 +12:00
Ali
5191fc9220 fix(app): explain rollback tooltip [EE-6825] (#11699)
Co-authored-by: testa113 <testa113>
2024-05-02 14:10:40 +12:00
Ali
af4e362c5c fix(version): reduce github requests [EE-7017] (#11678) 2024-05-02 14:08:44 +12:00
Matt Hook
eb5b9ef069 Revert "fix(kube): fix text in activity and authentication logs teasers [EE-6742]" (#11727) 2024-05-01 09:00:13 +12:00
Matt Hook
a74c6dbd24 fix(kube): fix text in activity and authentication logs teasers [EE-6742] (#11680) 2024-04-30 19:16:47 +12:00
cmeng
6451ccce94 fix(edge-stack): add completed status EE-6210 (#11633) 2024-04-30 13:44:18 +12:00
Ali
6dd5150e23 Revert "fix(app): avoid 'no label' error when deleting external app [EE-6019]" (#11696) 2024-04-26 08:51:46 +12:00
Ali
441db15cfd fix(app): avoid 'no label' error when deleting external app [EE-6019] (#11672) 2024-04-26 08:42:13 +12:00
Chaim Lev-Ari
b44fabaefe fix(users): return json from create token [EE-6856] (#11576) 2024-04-25 10:10:39 +03:00
Ali
ddeddc723e fix(migration): run post init migrations for edge after server starts [EE-6905] (#11547)
Co-authored-by: testa113 <testa113>
2024-04-23 16:15:33 +12:00
Matt Hook
e980ce3d6a fix(settings): fix crash during settings update when not using oauth [EE-7031] (#11660) 2024-04-23 12:58:13 +12:00
Oscar Zhou
123a138278 feat(setting/oauth): add authstyle option [EE-6038] (#11609) 2024-04-22 10:35:14 +12:00
Oscar Zhou
cc3ec3cebd fix(stack/git): option to overwrite target path during dir move [EE-6871] (#11623) 2024-04-22 10:34:44 +12:00
cmeng
5dab7a1df4 fix(docker-client): explicitly set docker client scheme EE-6935 (#11518) 2024-04-22 09:00:49 +12:00
Matt Hook
ed0cf4d79c chore(kubectl): update kubectl to latest point release [EE-7018] (#11621) 2024-04-19 11:47:11 +12:00
andres-portainer
aa4b8ad5e3 fix(workflows): upgrade Go to v1.21.9 EE-6939 (#11643) 2024-04-18 19:03:25 -03:00
Prabhat Khera
81811f669d fix(stack): fix stack env variable link [EE-6902] (#11625) 2024-04-19 07:00:19 +12:00
andres-portainer
3ae55d8c3e fix(mingit): upgrade to v2.44.0.1 EE-7023 (#11640) 2024-04-18 15:22:25 -03:00
andres-portainer
933c2a7002 fix(docker): upgrade to v24.0.9 EE-7016 (#11619) 2024-04-17 19:38:23 -03:00
andres-portainer
1641642695 fix(go): upgrade Go to v1.21.9 in the nightly security scan EE-6939 (#11616) 2024-04-17 18:09:41 -03:00
Matt Hook
f80b1ed53a fix(auth): prevent user enumeration attack [EE-6832] (#11587) 2024-04-17 16:08:56 +12:00
Prabhat Khera
d04da7898d fix(pending-actions): clean pending actions for deleted environment [EE-6545] (#11599) 2024-04-17 08:32:25 +12:00
Matt Hook
ec83d02afa chore(docker): bump docker client to 26.0.1 [EE-6941] (#11594) 2024-04-16 08:27:35 +12:00
Prabhat Khera
05265dda47 fix(stacks): update info text for stack environment variables [EE-6902] (#11557) 2024-04-16 08:03:46 +12:00
Prabhat Khera
74e1ff5e2d fix(pending-actions): fix create kubeclient to check endpoint status [EE-6545] (#11585) 2024-04-16 07:40:45 +12:00
Matt Hook
795d812652 chore(api): bump docker and protobuf pkgs [EE-6941] (#11549) 2024-04-15 10:52:52 +12:00
Prabhat Khera
46b1d5b528 fix(compose): update compose to 2.26.1 [EE-6546] (#11537)
* update compose to 2.24

* chore(unpacker): use APIVersion as unpacker image tag [EE-6974] (#11538)

---------

Co-authored-by: hookenz <hookenz@gmail.com>
2024-04-15 10:39:28 +12:00
Matt Hook
cf7672d59e bump helm version (#11563) 2024-04-15 09:18:18 +12:00
andres-portainer
9c8a30693a fix(protobuf): upgrade protobuf to v1.33 EE-6945 (#11568) 2024-04-12 17:52:52 -03:00
andres-portainer
023945cbd2 fix(go): upgrade Go to v1.21.9 EE-6939 (#11556) 2024-04-12 17:08:27 -03:00
Matt Hook
498ba46863 fix(backups): improved archive encryption [EE-6764] (#11482) 2024-04-10 10:58:16 +12:00
Matt Hook
399ddaea3b fix(services): speed up service count on the kubernetes dashboard [EE-6967] (#11524) 2024-04-09 15:50:39 +12:00
cmeng
13cee9975c feat(version): bump to 2.20.2 EE-6979 (#11517) 2024-04-08 12:27:51 +12:00
Matt Hook
f8927851e4 fix(apikey): don't authenticate api key for external auth [EE-6932] (#11461) 2024-04-08 11:06:34 +12:00
Oscar Zhou
b284d7094a fix(stack): filter out orphan stacks that have same name as normal stacks [EE-6791] (#11471) 2024-04-03 09:53:36 +13:00
LP B
7bb54bcbe6 fix(app): replace fields removed by Docker 25 and 26 (#11469)
* fix(app/volume): make optional Container and ContainerConfig fields removed in docker 26

* fix(app/image): use image.Size instead of image.VirtualSize removed in Docker 25
2024-03-29 13:57:18 +01:00
cmeng
b3c489366f fix(edge-stack): avoid reference of undefined EE-6914 (#11465) 2024-03-27 16:02:25 +13:00
cmeng
5eca761883 feat(version): bump to 2.20.1 EE-6933 (#11459) 2024-03-27 15:41:45 +13:00
andres-portainer
bea8acce1f fix(kubernetes): avoid a deadlock EE-6901 (#11446) 2024-03-25 14:19:33 -03:00
172 changed files with 1521 additions and 2260 deletions

View File

@@ -22,7 +22,7 @@ on:
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.21.11
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:

View File

@@ -5,7 +5,6 @@ env:
NODE_VERSION: 18.x
on:
workflow_dispatch:
pull_request:
branches:
- master
@@ -28,22 +27,15 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up node.js'
uses: actions/setup-node@v4.0.1
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
test-server:
strategy:
matrix:
@@ -56,21 +48,9 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up golang'
uses: actions/setup-go@v5.0.0
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: 'install dependencies'
run: make test-deps PLATFORM=linux ARCH=amd64
- name: 'update $PATH'
run: echo "$(pwd)/dist" >> $GITHUB_PATH
- name: 'run tests'
- name: Run tests
run: make test-server

View File

@@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from 'react-query';
initMSW(
{
onUnhandledRequest: ({ method, url }) => {
console.log(method, url);
if (url.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.

View File

@@ -64,9 +64,6 @@ clean: ## Remove all build and download artifacts
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-deps: init-dist
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
test-client: ## Run client tests
yarn test $(ARGS)

View File

@@ -82,8 +82,7 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
}
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
dbFileName := datastore.Connection().GetDatabaseFileName()
_, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName))
_, err := datastore.Backup(filepath.Join(backupDirPath, "portainer.db"))
return err
}

View File

@@ -5,17 +5,6 @@ import (
"github.com/portainer/portainer/api/internal/edge/cache"
)
// EdgeJobs retrieves the edge jobs for the given environment
func (service *Service) EdgeJobs(endpointID portainer.EndpointID) []portainer.EdgeJob {
service.mu.RLock()
defer service.mu.RUnlock()
return append(
make([]portainer.EdgeJob, 0, len(service.edgeJobs[endpointID])),
service.edgeJobs[endpointID]...,
)
}
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
@@ -23,10 +12,10 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
}
service.mu.Lock()
defer service.mu.Unlock()
tunnel := service.getTunnelDetails(endpoint.ID)
existingJobIndex := -1
for idx, existingJob := range service.edgeJobs[endpoint.ID] {
for idx, existingJob := range tunnel.Jobs {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
@@ -35,28 +24,30 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
}
if existingJobIndex == -1 {
service.edgeJobs[endpoint.ID] = append(service.edgeJobs[endpoint.ID], *edgeJob)
tunnel.Jobs = append(tunnel.Jobs, *edgeJob)
} else {
service.edgeJobs[endpoint.ID][existingJobIndex] = *edgeJob
tunnel.Jobs[existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
service.mu.Unlock()
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for endpointID := range service.edgeJobs {
for endpointID, tunnel := range service.tunnelDetailsMap {
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
tunnel.Jobs[n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
}
@@ -66,17 +57,19 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
defer service.mu.Unlock()
tunnel := service.getTunnelDetails(endpointID)
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
tunnel.Jobs[n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
service.mu.Unlock()
}

View File

@@ -19,6 +19,7 @@ import (
const (
tunnelCleanupInterval = 10 * time.Second
requiredTimeout = 15 * time.Second
activeTimeout = 4*time.Minute + 30*time.Second
pingTimeout = 3 * time.Second
)
@@ -27,54 +28,32 @@ const (
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
// connected to the tunnel server.
type Service struct {
serverFingerprint string
serverPort string
activeTunnels map[portainer.EndpointID]*portainer.TunnelDetails
edgeJobs map[portainer.EndpointID][]portainer.EdgeJob
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.RWMutex
fileService portainer.FileService
defaultCheckinInterval int
serverFingerprint string
serverPort string
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.Mutex
fileService portainer.FileService
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
defaultCheckinInterval := portainer.DefaultEdgeAgentCheckinIntervalInSeconds
settings, err := dataStore.Settings().Settings()
if err == nil {
defaultCheckinInterval = settings.EdgeAgentCheckinInterval
} else {
log.Error().Err(err).Msg("unable to retrieve the settings from the database")
}
return &Service{
activeTunnels: make(map[portainer.EndpointID]*portainer.TunnelDetails),
edgeJobs: make(map[portainer.EndpointID][]portainer.EdgeJob),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
fileService: fileService,
defaultCheckinInterval: defaultCheckinInterval,
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
fileService: fileService,
}
}
// pingAgent ping the given agent so that the agent can keep the tunnel alive
func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
tunnelAddr, err := service.TunnelAddr(endpoint)
if err != nil {
return err
}
requestURL := fmt.Sprintf("http://%s/ping", tunnelAddr)
tunnel := service.GetTunnelDetails(endpointID)
requestURL := fmt.Sprintf("http://127.0.0.1:%d/ping", tunnel.Port)
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
if err != nil {
return err
@@ -97,49 +76,47 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
go service.keepTunnelAlive(endpointID, ctx, maxAlive)
}
go func() {
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("max_alive_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: start")
func (service *Service) keepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("max_alive_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: start")
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
for {
select {
case <-pingTicker.C:
service.SetTunnelStatusToActive(endpointID)
err := service.pingAgent(endpointID)
if err != nil {
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: ping agent")
}
case <-maxAliveTicker.C:
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("timeout_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: tunnel keep alive timeout")
for {
select {
case <-pingTicker.C:
service.UpdateLastActivity(endpointID)
if err := service.pingAgent(endpointID); err != nil {
return
case <-ctx.Done():
err := ctx.Err()
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: ping agent")
Msg("KeepTunnelAlive: tunnel stop")
return
}
case <-maxAliveTicker.C:
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("timeout_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: tunnel keep alive timeout")
return
case <-ctx.Done():
err := ctx.Err()
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: tunnel stop")
return
}
}
}()
}
// StartTunnelServer starts a tunnel server on the specified addr and port.
@@ -149,6 +126,7 @@ func (service *Service) keepTunnelAlive(endpointID portainer.EndpointID, ctx con
// The snapshotter is used in the tunnel status verification process.
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
privateKeyFile, err := service.retrievePrivateKeyFile()
if err != nil {
return err
}
@@ -166,21 +144,21 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
service.serverFingerprint = chiselServer.GetFingerprint()
service.serverPort = port
if err := chiselServer.Start(addr, port); err != nil {
err = chiselServer.Start(addr, port)
if err != nil {
return err
}
service.chiselServer = chiselServer
// TODO: work-around Chisel default behavior.
// By default, Chisel will allow anyone to connect if no user exists.
username, password := generateRandomCredentials()
if err = service.chiselServer.AddUser(username, password, "127.0.0.1"); err != nil {
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
if err != nil {
return err
}
service.snapshotService = snapshotService
go service.startTunnelVerificationLoop()
return nil
@@ -194,39 +172,37 @@ func (service *Service) StopTunnelServer() error {
func (service *Service) retrievePrivateKeyFile() (string, error) {
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
if exists, _ := service.fileService.FileExists(privateKeyFile); exists {
exist, _ := service.fileService.FileExists(privateKeyFile)
if !exist {
log.Debug().
Str("private-key", privateKeyFile).
Msg("Chisel private key file does not exist")
privateKey, err := ccrypto.GenerateKey("")
if err != nil {
log.Error().
Err(err).
Msg("Failed to generate chisel private key")
return "", err
}
err = service.fileService.StoreChiselPrivateKey(privateKey)
if err != nil {
log.Error().
Err(err).
Msg("Failed to save Chisel private key to disk")
return "", err
} else {
log.Info().
Str("private-key", privateKeyFile).
Msg("Generated a new Chisel private key file")
}
} else {
log.Info().
Str("private-key", privateKeyFile).
Msg("found Chisel private key file on disk")
return privateKeyFile, nil
Msg("Found Chisel private key file on disk")
}
log.Debug().
Str("private-key", privateKeyFile).
Msg("chisel private key file does not exist")
privateKey, err := ccrypto.GenerateKey("")
if err != nil {
log.Error().
Err(err).
Msg("failed to generate chisel private key")
return "", err
}
if err = service.fileService.StoreChiselPrivateKey(privateKey); err != nil {
log.Error().
Err(err).
Msg("failed to save Chisel private key to disk")
return "", err
}
log.Info().
Str("private-key", privateKeyFile).
Msg("generated a new Chisel private key file")
return privateKeyFile, nil
}
@@ -254,45 +230,63 @@ func (service *Service) startTunnelVerificationLoop() {
}
}
// checkTunnels finds the first tunnel that has not had any activity recently
// and attempts to take a snapshot, then closes it and returns
func (service *Service) checkTunnels() {
service.mu.RLock()
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
for endpointID, tunnel := range service.activeTunnels {
elapsed := time.Since(tunnel.LastActivity)
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("last_activity_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed < activeTimeout {
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
tunnelPort := tunnel.Port
service.mu.RUnlock()
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("last_activity_seconds", elapsed.Seconds()).
Float64("timeout_seconds", activeTimeout.Seconds()).
Msg("last activity timeout exceeded")
if err := service.snapshotEnvironment(endpointID, tunnelPort); err != nil {
log.Error().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("unable to snapshot Edge environment")
if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
continue
}
service.close(portainer.EndpointID(endpointID))
if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
continue
}
return
tunnels[key] = *tunnel
}
service.mu.Unlock()
service.mu.RUnlock()
for endpointID, tunnel := range tunnels {
elapsed := time.Since(tunnel.LastActivity)
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", requiredTimeout.Seconds()).
Msg("REQUIRED state timeout exceeded")
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", activeTimeout.Seconds()).
Msg("ACTIVE state timeout exceeded")
err := service.snapshotEnvironment(endpointID, tunnel.Port)
if err != nil {
log.Error().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("unable to snapshot Edge environment")
}
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
}
}
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {

View File

@@ -7,22 +7,14 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/require"
)
func TestPingAgentPanic(t *testing.T) {
endpoint := &portainer.Endpoint{
ID: 1,
EdgeID: "test-edge-id",
Type: portainer.EdgeAgentOnDockerEnvironment,
UserTrusted: true,
}
endpointID := portainer.EndpointID(1)
_, store := datastore.MustNewTestStore(t, true, true)
s := NewService(store, nil, nil)
s := NewService(nil, nil, nil)
defer func() {
require.Nil(t, recover())
@@ -40,9 +32,8 @@ func TestPingAgentPanic(t *testing.T) {
require.NoError(t, http.Serve(ln, mux))
}()
err = s.Open(endpoint)
require.NoError(t, err)
s.activeTunnels[endpoint.ID].Port = ln.Addr().(*net.TCPAddr).Port
s.getTunnelDetails(endpointID)
s.tunnelDetailsMap[endpointID].Port = ln.Addr().(*net.TCPAddr).Port
require.Error(t, s.pingAgent(endpoint.ID))
require.Error(t, s.pingAgent(endpointID))
}

View File

@@ -5,18 +5,14 @@ import (
"errors"
"fmt"
"math/rand"
"net"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/pkg/libcrypto"
"github.com/dchest/uniuri"
"github.com/rs/zerolog/log"
)
const (
@@ -24,191 +20,18 @@ const (
maxAvailablePort = 65535
)
var (
ErrNonEdgeEnv = errors.New("cannot open a tunnel for non-edge environments")
ErrAsyncEnv = errors.New("cannot open a tunnel for async edge environments")
ErrInvalidEnv = errors.New("cannot open a tunnel for an invalid environment")
)
// Open will mark the tunnel as REQUIRED so the agent opens it
func (s *Service) Open(endpoint *portainer.Endpoint) error {
if !endpointutils.IsEdgeEndpoint(endpoint) {
return ErrNonEdgeEnv
}
if endpoint.Edge.AsyncMode {
return ErrAsyncEnv
}
if endpoint.ID == 0 || endpoint.EdgeID == "" || !endpoint.UserTrusted {
return ErrInvalidEnv
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.activeTunnels[endpoint.ID]; ok {
return nil
}
defer cache.Del(endpoint.ID)
tun := &portainer.TunnelDetails{
Status: portainer.EdgeAgentManagementRequired,
Port: s.getUnusedPort(),
LastActivity: time.Now(),
}
username, password := generateRandomCredentials()
if s.chiselServer != nil {
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tun.Port)
if err := s.chiselServer.AddUser(username, password, authorizedRemote); err != nil {
return err
}
}
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
if err != nil {
return err
}
tun.Credentials = credentials
s.activeTunnels[endpoint.ID] = tun
return nil
}
// close removes the tunnel from the map so the agent will close it
func (s *Service) close(endpointID portainer.EndpointID) {
s.mu.Lock()
defer s.mu.Unlock()
tun, ok := s.activeTunnels[endpointID]
if !ok {
return
}
if len(tun.Credentials) > 0 && s.chiselServer != nil {
user, _, _ := strings.Cut(tun.Credentials, ":")
s.chiselServer.DeleteUser(user)
}
if s.ProxyManager != nil {
s.ProxyManager.DeleteEndpointProxy(endpointID)
}
delete(s.activeTunnels, endpointID)
cache.Del(endpointID)
}
// Config returns the tunnel details needed for the agent to connect
func (s *Service) Config(endpointID portainer.EndpointID) portainer.TunnelDetails {
s.mu.RLock()
defer s.mu.RUnlock()
if tun, ok := s.activeTunnels[endpointID]; ok {
return *tun
}
return portainer.TunnelDetails{Status: portainer.EdgeAgentIdle}
}
// TunnelAddr returns the address of the local tunnel, including the port, it
// will block until the tunnel is ready
func (s *Service) TunnelAddr(endpoint *portainer.Endpoint) (string, error) {
if err := s.Open(endpoint); err != nil {
return "", err
}
tun := s.Config(endpoint.ID)
checkinInterval := time.Duration(s.tryEffectiveCheckinInterval(endpoint)) * time.Second
for t0 := time.Now(); ; {
if time.Since(t0) > 2*checkinInterval {
s.close(endpoint.ID)
return "", errors.New("unable to open the tunnel")
}
// Check if the tunnel is established
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: tun.Port})
if err != nil {
time.Sleep(checkinInterval / 100)
continue
}
conn.Close()
break
}
s.UpdateLastActivity(endpoint.ID)
return fmt.Sprintf("127.0.0.1:%d", tun.Port), nil
}
// tryEffectiveCheckinInterval avoids a potential deadlock by returning a
// previous known value after a timeout
func (s *Service) tryEffectiveCheckinInterval(endpoint *portainer.Endpoint) int {
ch := make(chan int, 1)
go func() {
ch <- edge.EffectiveCheckinInterval(s.dataStore, endpoint)
}()
select {
case <-time.After(50 * time.Millisecond):
s.mu.RLock()
defer s.mu.RUnlock()
return s.defaultCheckinInterval
case i := <-ch:
s.mu.Lock()
s.defaultCheckinInterval = i
s.mu.Unlock()
return i
}
}
// UpdateLastActivity sets the current timestamp to avoid the tunnel timeout
func (s *Service) UpdateLastActivity(endpointID portainer.EndpointID) {
s.mu.Lock()
defer s.mu.Unlock()
if tun, ok := s.activeTunnels[endpointID]; ok {
tun.LastActivity = time.Now()
}
}
// NOTE: it needs to be called with the lock acquired
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
port := randomInt(minAvailablePort, maxAvailablePort)
for _, tunnel := range service.activeTunnels {
for _, tunnel := range service.tunnelDetailsMap {
if tunnel.Port == port {
return service.getUnusedPort()
}
}
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port})
if err == nil {
conn.Close()
log.Debug().
Int("port", port).
Msg("selected port is in use, trying a different one")
return service.getUnusedPort()
}
return port
}
@@ -216,10 +39,152 @@ func randomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// NOTE: it needs to be called with the lock acquired
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
return tunnel
}
tunnel := &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
}
service.tunnelDetailsMap[endpointID] = tunnel
cache.Del(endpointID)
return tunnel
}
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
service.mu.Lock()
defer service.mu.Unlock()
return *service.getTunnelDetails(endpointID)
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
if endpoint.Edge.AsyncMode {
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
}
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
// update the LastActivity
service.SetTunnelStatusToActive(endpoint.ID)
}
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
return service.GetTunnelDetails(endpoint.ID), nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
credentials := tunnel.Credentials
if credentials != "" {
tunnel.Credentials = ""
if service.chiselServer != nil {
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
}
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to REQUIRED.
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
defer cache.Del(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()
defer service.mu.Unlock()
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
tunnel.Status = portainer.EdgeAgentManagementRequired
tunnel.Port = service.getUnusedPort()
tunnel.LastActivity = time.Now()
username, password := generateRandomCredentials()
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
if service.chiselServer != nil {
err = service.chiselServer.AddUser(username, password, authorizedRemote)
if err != nil {
return err
}
}
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
if err != nil {
return err
}
tunnel.Credentials = credentials
}
return nil
}
func generateRandomCredentials() (string, string) {
username := uniuri.NewLen(8)
password := uniuri.NewLen(8)
return username, password
}

View File

@@ -62,7 +62,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
}
kingpin.Parse()

View File

@@ -42,13 +42,6 @@ func setLoggingMode(mode string) {
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage,
})
case "NOCOLOR":
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage,
NoColor: true,
})
case "JSON":
log.Logger = log.Output(os.Stderr)
}

View File

@@ -22,12 +22,6 @@ func CreateTLSConfiguration() *tls.Config {
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
},
}
}

View File

@@ -36,7 +36,6 @@ type (
}
DataStore interface {
Connection() portainer.Connection
Open() (newStore bool, err error)
Init() error
Close() error

View File

@@ -941,6 +941,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.21.0-rc2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.20.3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -3,6 +3,7 @@ package client
import (
"bytes"
"errors"
"fmt"
"io"
"maps"
"net/http"
@@ -49,12 +50,12 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
case portainer.AgentOnDockerEnvironment:
return createAgentClient(endpoint, endpoint.URL, factory.signatureService, nodeName, timeout)
case portainer.EdgeAgentOnDockerEnvironment:
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, err
}
endpointURL := "http://" + tunnelAddr
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
return createAgentClient(endpoint, endpointURL, factory.signatureService, nodeName, timeout)
}

View File

@@ -3,6 +3,7 @@ package exec
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path"
@@ -185,11 +186,11 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
tunnelAddr, err := manager.reverseTunnelService.TunnelAddr(endpoint)
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return "", nil, err
}
endpointURL = "tcp://" + tunnelAddr
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}
args = append(args, "-H", endpointURL)

View File

@@ -4,7 +4,6 @@ import (
"crypto/rand"
"fmt"
"net/http"
"os"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -14,13 +13,6 @@ import (
)
func WithProtect(handler http.Handler) (http.Handler, error) {
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
isDockerDesktopExtension := false
if val, ok := os.LookupEnv("DOCKER_EXTENSION"); ok && val == "1" {
isDockerDesktopExtension = true
}
handler = withSendCSRFToken(handler)
token := make([]byte, 32)
@@ -35,7 +27,7 @@ func WithProtect(handler http.Handler) (http.Handler, error) {
gorillacsrf.Secure(false),
)(handler)
return withSkipCSRF(handler, isDockerDesktopExtension), nil
return withSkipCSRF(handler), nil
}
func withSendCSRFToken(handler http.Handler) http.Handler {
@@ -56,10 +48,10 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
})
}
func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Handler {
func withSkipCSRF(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension)
skip, err := security.ShouldSkipCSRFCheck(r)
if err != nil {
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)
return

View File

@@ -70,7 +70,8 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re
if err != nil {
log.Warn().Err(err).Msg("failed to download git repository")
if rbErr := rollbackCustomTemplate(backupPath, customTemplate.ProjectPath); rbErr != nil {
if err != nil {
rbErr := rollbackCustomTemplate(backupPath, customTemplate.ProjectPath)
return httperror.InternalServerError("Failed to rollback the custom template folder", rbErr)
}

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"bytes"
"errors"
"io"
"io/fs"
"net/http"
@@ -20,7 +19,6 @@ import (
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
@@ -51,19 +49,6 @@ func (f *TestFileService) GetFileContent(projectPath, configFilePath string) ([]
return os.ReadFile(filepath.Join(projectPath, configFilePath))
}
type InvalidTestGitService struct {
portainer.GitService
targetFilePath string
}
func (g *InvalidTestGitService) CloneRepository(dest, repoUrl, refName, username, password string, tlsSkipVerify bool) error {
return errors.New("simulate network error")
}
func (g *InvalidTestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
return "", nil
}
func createTestFile(targetPath string) error {
f, err := os.Create(targetPath)
if err != nil {
@@ -189,28 +174,4 @@ func Test_customTemplateGitFetch(t *testing.T) {
singleAPIRequest(h, jwt2, is, "gfedcba")
})
t.Run("restore git repository if it is failed to download the new git repository", func(t *testing.T) {
invalidGitService := &InvalidTestGitService{
targetFilePath: filepath.Join(template1.ProjectPath, template1.GitConfig.ConfigFilePath),
}
h := NewHandler(requestBouncer, store, fileService, invalidGitService)
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBuffer([]byte("{}")))
testhelpers.AddTestSecurityCookie(req, jwt1)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
is.Equal(http.StatusInternalServerError, rr.Code)
var errResp httperror.HandlerError
err = json.NewDecoder(rr.Body).Decode(&errResp)
assert.NoError(t, err, "failed to parse error body")
assert.FileExists(t, gitService.targetFilePath, "previous git repository is not restored")
fileContent, err := os.ReadFile(gitService.targetFilePath)
assert.NoError(t, err, "failed to read target file")
assert.Equal(t, "gfedcba", string(fileContent))
})
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/docker"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
)
@@ -31,7 +30,7 @@ func NewHandler(routePrefix string, bouncer security.BouncerService, dataStore d
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess, middlewares.CheckEndpointAuthorization(bouncer))
router.Use(bouncer.AuthenticatedAccess)
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
router.Handle("/{containerId}/recreate", httperror.LoggerHandler(h.recreate)).Methods(http.MethodPost)

View File

@@ -40,14 +40,14 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
}
// endpoints
endpointRouter := h.PathPrefix("/docker/{id}").Subrouter()
endpointRouter := h.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(dockerOnlyMiddleware)
containersHandler := containers.NewHandler("/docker/{id}/containers", bouncer, dataStore, dockerClientFactory, containerService)
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dataStore, dockerClientFactory, containerService)
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
imagesHandler := images.NewHandler("/docker/{id}/images", bouncer, dockerClientFactory)
imagesHandler := images.NewHandler("/{id}/images", bouncer, dockerClientFactory)
endpointRouter.PathPrefix("/images").Handler(imagesHandler)
return h
}

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -26,7 +25,7 @@ func NewHandler(routePrefix string, bouncer security.BouncerService, dockerClien
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess, middlewares.CheckEndpointAuthorization(bouncer))
router.Use(bouncer.AuthenticatedAccess)
router.Handle("", httperror.LoggerHandler(h.imagesList)).Methods(http.MethodGet)
return h

View File

@@ -19,9 +19,8 @@ import (
// @security jwt
// @param id path int true "EdgeGroup Id"
// @success 204
// @failure 409 "Edge group is in use by an Edge stack or Edge job"
// @failure 503 "Edge compute features are disabled"
// @failure 500 "Server error"
// @failure 500
// @router /edge_groups/{id} [delete]
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")

View File

@@ -15,13 +15,10 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/cache"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
type stackStatusResponse struct {
@@ -95,8 +92,6 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
}
firstConn := endpoint.LastCheckInDate == 0
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
@@ -111,7 +106,7 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
var statusResponse *endpointEdgeStatusInspectResponse
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
statusResponse, err = handler.inspectStatus(tx, r, portainer.EndpointID(endpointID), firstConn)
statusResponse, err = handler.inspectStatus(tx, r, portainer.EndpointID(endpointID))
return err
})
if err != nil {
@@ -126,7 +121,7 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
return cacheResponse(w, endpoint.ID, *statusResponse)
}
func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Request, endpointID portainer.EndpointID, firstConn bool) (*endpointEdgeStatusInspectResponse, error) {
func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Request, endpointID portainer.EndpointID) (*endpointEdgeStatusInspectResponse, error) {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
return nil, err
@@ -138,10 +133,8 @@ func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Reque
}
// Take an initial snapshot
if firstConn {
if err := handler.ReverseTunnelService.Open(endpoint); err != nil {
log.Error().Err(err).Msg("could not open the tunnel")
}
if endpoint.LastCheckInDate == 0 {
handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
}
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
@@ -160,21 +153,34 @@ func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Reque
return nil, httperror.InternalServerError("Unable to persist environment changes inside the database", err)
}
tunnel := handler.ReverseTunnelService.Config(endpoint.ID)
checkinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
settings, err := tx.Settings().Settings()
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
checkinInterval = settings.EdgeAgentCheckinInterval
}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
statusResponse := endpointEdgeStatusInspectResponse{
Status: tunnel.Status,
Port: tunnel.Port,
CheckinInterval: edge.EffectiveCheckinInterval(tx, endpoint),
CheckinInterval: checkinInterval,
Credentials: tunnel.Credentials,
}
schedules, handlerErr := handler.buildSchedules(endpoint.ID)
schedules, handlerErr := handler.buildSchedules(endpoint.ID, tunnel)
if handlerErr != nil {
return nil, handlerErr
}
statusResponse.Schedules = schedules
if tunnel.Status == portainer.EdgeAgentManagementRequired {
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
}
edgeStacksStatus, handlerErr := handler.buildEdgeStacks(tx, endpoint.ID)
if handlerErr != nil {
return nil, handlerErr
@@ -207,9 +213,9 @@ func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
}
}
func (handler *Handler) buildSchedules(endpointID portainer.EndpointID) ([]edgeJobResponse, *httperror.HandlerError) {
func (handler *Handler) buildSchedules(endpointID portainer.EndpointID, tunnel portainer.TunnelDetails) ([]edgeJobResponse, *httperror.HandlerError) {
schedules := []edgeJobResponse{}
for _, job := range handler.ReverseTunnelService.EdgeJobs(endpointID) {
for _, job := range tunnel.Jobs {
var collectLogs bool
if _, ok := job.GroupLogsCollection[endpointID]; ok {
collectLogs = job.GroupLogsCollection[endpointID].CollectLogs

View File

@@ -34,7 +34,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
return httperror.InternalServerError("No Edge agent registered with the environment", errors.New("No agent available"))
}
_, err := handler.ReverseTunnelService.TunnelAddr(endpoint)
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return httperror.InternalServerError("Unable to get the active tunnel", err)
}

View File

@@ -34,7 +34,7 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
return httperror.InternalServerError("No Edge agent registered with the environment", errors.New("No agent available"))
}
_, err := handler.ReverseTunnelService.TunnelAddr(endpoint)
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return httperror.InternalServerError("Unable to get the active tunnel", err)
}

View File

@@ -59,6 +59,8 @@ func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http
return httperror.InternalServerError("Failed persisting environment in database", err)
}
handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID)
return response.Empty(w)
}

View File

@@ -201,7 +201,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @param Gpus formData string false "List of GPUs - json stringified array of {name, value} structs"
// @success 200 {object} portainer.Endpoint "Success"
// @failure 400 "Invalid request"
// @failure 409 "Name is not unique"
// @failure 500 "Server error"
// @router /endpoints [post]
func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -2,7 +2,6 @@ package endpoints
import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
@@ -18,40 +17,19 @@ import (
"github.com/rs/zerolog/log"
)
type endpointDeleteRequest struct {
ID int `json:"id"`
DeleteCluster bool `json:"deleteCluster"`
}
type endpointDeleteBatchPayload struct {
Endpoints []endpointDeleteRequest `json:"endpoints"`
}
type endpointDeleteBatchPartialResponse struct {
Deleted []int `json:"deleted"`
Errors []int `json:"errors"`
}
func (payload *endpointDeleteBatchPayload) Validate(r *http.Request) error {
if payload == nil || len(payload.Endpoints) == 0 {
return fmt.Errorf("invalid request payload. You must provide a list of environments to delete")
}
return nil
}
// @id EndpointDelete
// @summary Remove an environment
// @description Remove the environment associated to the specified identifier and optionally clean-up associated resources.
// @description **Access policy**: Administrator only.
// @summary Remove an environment(endpoint)
// @description Remove an environment(endpoint).
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth || jwt
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Environment successfully deleted."
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 404 "Unable to find the environment with the specified identifier inside the database."
// @failure 500 "Server error occurred while attempting to delete the environment."
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/{id} [delete]
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
@@ -84,63 +62,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return response.Empty(w)
}
// @id EndpointDeleteBatch
// @summary Remove multiple environments
// @description Remove multiple environments and optionally clean-up associated resources.
// @description **Access policy**: Administrator only.
// @tags endpoints
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param body body endpointDeleteBatchPayload true "List of environments to delete, with optional deleteCluster flag to clean-up assocaited resources (cloud environments only)"
// @success 204 "Environment(s) successfully deleted."
// @failure 207 {object} endpointDeleteBatchPartialResponse "Partial success. Some environments were deleted successfully, while others failed."
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error occurred while attempting to delete the specified environments."
// @router /endpoints [delete]
func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var p endpointDeleteBatchPayload
if err := request.DecodeAndValidateJSONPayload(r, &p); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
resp := endpointDeleteBatchPartialResponse{
Deleted: []int{},
Errors: []int{},
}
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
for _, e := range p.Endpoints {
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(e.ID)) {
resp.Errors = append(resp.Errors, e.ID)
log.Warn().Err(httperrors.ErrNotAvailableInDemo).Msgf("Unable to remove demo environment %d", e.ID)
continue
}
if err := handler.deleteEndpoint(tx, portainer.EndpointID(e.ID), e.DeleteCluster); err != nil {
resp.Errors = append(resp.Errors, e.ID)
log.Warn().Err(err).Int("environment_id", e.ID).Msg("Unable to remove environment")
continue
}
resp.Deleted = append(resp.Deleted, e.ID)
}
return nil
}); err != nil {
return httperror.InternalServerError("Unable to delete environments", err)
}
if len(resp.Errors) > 0 {
return response.JSONWithStatus(w, resp, http.StatusPartialContent)
}
return response.Empty(w)
}
func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID portainer.EndpointID, deleteCluster bool) error {
endpoint, err := tx.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if tx.IsErrObjectNotFound(err) {
@@ -157,20 +78,23 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
}
if err := tx.Snapshot().Delete(endpointID); err != nil {
log.Warn().Err(err).Msg("Unable to remove the snapshot from the database")
err = tx.Snapshot().Delete(endpointID)
if err != nil {
log.Warn().Err(err).Msgf("Unable to remove the snapshot from the database")
}
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
if err := handler.AuthorizationService.UpdateUsersAuthorizationsTx(tx); err != nil {
log.Warn().Err(err).Msg("Unable to update user authorizations")
err = handler.AuthorizationService.UpdateUsersAuthorizationsTx(tx)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update user authorizations")
}
}
if err := tx.EndpointRelation().DeleteEndpointRelation(endpoint.ID); err != nil {
log.Warn().Err(err).Msg("Unable to remove environment relation from the database")
err = tx.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
if err != nil {
log.Warn().Err(err).Msgf("Unable to remove environment relation from the database")
}
for _, tagID := range endpoint.TagIDs {
@@ -182,9 +106,9 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
if handler.DataStore.IsErrObjectNotFound(err) {
log.Warn().Err(err).Msg("Unable to find tag inside the database")
log.Warn().Err(err).Msgf("Unable to find tag inside the database")
} else if err != nil {
log.Warn().Err(err).Msg("Unable to delete tag relation from the database")
log.Warn().Err(err).Msgf("Unable to delete tag relation from the database")
}
}
@@ -198,39 +122,40 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
return e == endpoint.ID
})
if err := tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup); err != nil {
log.Warn().Err(err).Msg("Unable to update edge group")
err = tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update edge group")
}
}
edgeStacks, err := tx.EdgeStack().EdgeStacks()
if err != nil {
log.Warn().Err(err).Msg("Unable to retrieve edge stacks from the database")
log.Warn().Err(err).Msgf("Unable to retrieve edge stacks from the database")
}
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
if err := tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack); err != nil {
log.Warn().Err(err).Msg("Unable to update edge stack")
err = tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update edge stack")
}
}
}
registries, err := tx.Registry().ReadAll()
if err != nil {
log.Warn().Err(err).Msg("Unable to retrieve registries from the database")
log.Warn().Err(err).Msgf("Unable to retrieve registries from the database")
}
for idx := range registries {
registry := &registries[idx]
if _, ok := registry.RegistryAccesses[endpoint.ID]; ok {
delete(registry.RegistryAccesses, endpoint.ID)
if err := tx.Registry().Update(registry.ID, registry); err != nil {
log.Warn().Err(err).Msg("Unable to update registry accesses")
err = tx.Registry().Update(registry.ID, registry)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update registry accesses")
}
}
}
@@ -238,7 +163,7 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
if endpointutils.IsEdgeEndpoint(endpoint) {
edgeJobs, err := handler.DataStore.EdgeJob().ReadAll()
if err != nil {
log.Warn().Err(err).Msg("Unable to retrieve edge jobs from the database")
log.Warn().Err(err).Msgf("Unable to retrieve edge jobs from the database")
}
for idx := range edgeJobs {
@@ -246,16 +171,18 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
if _, ok := edgeJob.Endpoints[endpoint.ID]; ok {
delete(edgeJob.Endpoints, endpoint.ID)
if err := tx.EdgeJob().Update(edgeJob.ID, edgeJob); err != nil {
log.Warn().Err(err).Msg("Unable to update edge job")
err = tx.EdgeJob().Update(edgeJob.ID, edgeJob)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update edge job")
}
}
}
}
// delete the pending actions
if err := tx.PendingActions().DeleteByEndpointID(endpoint.ID); err != nil {
log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msg("Unable to delete pending actions")
err = tx.PendingActions().DeleteByEndpointID(endpoint.ID)
if err != nil {
log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msgf("Unable to delete pending actions")
}
err = tx.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID))

View File

@@ -3,16 +3,15 @@ package endpoints
import (
"net/http"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/pkg/errors"
)
// @id endpointRegistriesList
@@ -128,7 +127,7 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
return true, nil
}
if !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace && namespace == kubernetes.DefaultNamespace {
if namespace == "default" {
return true, nil
}

View File

@@ -69,7 +69,6 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
// @success 200 {object} portainer.Endpoint "Success"
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 409 "Name is not unique"
// @failure 500 "Server error"
// @router /endpoints/{id} [put]
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -334,16 +334,11 @@ func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portai
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if edgeCheckinInterval == 0 {
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if endpoint.Edge.AsyncMode {
edgeCheckinInterval = getShortestAsyncInterval(&endpoint, settings)
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
@@ -634,29 +629,3 @@ func getEdgeStackStatusParam(r *http.Request) (*portainer.EdgeStackStatusType, e
return &edgeStackStatus, nil
}
func getShortestAsyncInterval(endpoint *portainer.Endpoint, settings *portainer.Settings) int {
var edgeIntervalUseDefault int = -1
pingInterval := endpoint.Edge.PingInterval
if pingInterval == edgeIntervalUseDefault {
pingInterval = settings.Edge.PingInterval
}
shortestAsyncInterval := pingInterval
snapshotInterval := endpoint.Edge.SnapshotInterval
if snapshotInterval == edgeIntervalUseDefault {
snapshotInterval = settings.Edge.SnapshotInterval
}
if shortestAsyncInterval > snapshotInterval {
shortestAsyncInterval = snapshotInterval
}
commandInterval := endpoint.Edge.CommandInterval
if commandInterval == edgeIntervalUseDefault {
commandInterval = settings.Edge.CommandInterval
}
if shortestAsyncInterval > commandInterval {
shortestAsyncInterval = commandInterval
}
return shortestAsyncInterval
}

View File

@@ -71,8 +71,6 @@ func NewHandler(bouncer security.BouncerService, demoService *demo.Service) *Han
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteBatch))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/dockerhub/{registryId}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}/snapshot",

View File

@@ -85,7 +85,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.21.0
// @version 2.20.3
// @description.markdown api-description.md
// @termsOfService
@@ -199,7 +199,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/docker"):
http.StripPrefix("/api", h.DockerHandler).ServeHTTP(w, r)
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):

View File

@@ -7,16 +7,12 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)
func hideFields(registry *portainer.Registry, hideAccesses bool) {
@@ -87,88 +83,29 @@ func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Re
return hasSameUrl && hasSameCredentials && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
}
// this function validates that
//
// 1. user has the appropriate authorizations to perform the request
//
// 2. user has a direct or indirect access to the registry
func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portainer.Registry) (hasAccess bool, isAdmin bool, err error) {
func (handler *Handler) userHasRegistryAccess(r *http.Request) (hasAccess bool, isAdmin bool, err error) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return false, false, err
}
user, err := handler.DataStore.User().Read(securityContext.UserID)
if err != nil {
return false, false, err
}
// Portainer admins always have access to everything
if securityContext.IsAdmin {
return true, true, nil
}
// mandatory query param that should become a path param
endpointIdStr, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return false, false, err
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err != nil {
return false, false, err
}
endpointId := portainer.EndpointID(endpointIdStr)
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointId)
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return false, false, err
}
// validate that the request is allowed for the user (READ/WRITE authorization on request path)
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); errors.Is(err, security.ErrAuthorizationRequired) {
return false, false, nil
} else if err != nil {
return false, false, err
}
memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return false, false, nil
}
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
if endpointutils.IsKubernetesEndpoint(endpoint) {
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
}
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve environment's namespaces policies to validate registry access")
}
authorizedNamespaces := registry.RegistryAccesses[endpointId].Namespaces
for _, namespace := range authorizedNamespaces {
// when the default namespace is authorized to use a registry, all users have the ability to use it
// unless the default namespace is restricted: in this case continue to search for other potential accesses authorizations
if namespace == kubernetes.DefaultNamespace && !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
return true, false, nil
}
namespacePolicy := accessPolicies[namespace]
if security.AuthorizedAccess(user.ID, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies) {
return true, false, nil
}
}
return false, false, nil
}
// validate access for docker environments
// leverage registry.RegistryAccesses[endpointId].UserAccessPolicies (direct access)
// and registry.RegistryAccesses[endpointId].TeamAccessPolicies (indirect access via his teams)
if security.AuthorizedRegistryAccess(registry, user, memberships, endpoint.ID) {
return true, false, nil
}
// when user has no access via their role, direct grant or indirect grant
// then they don't have access to the registry
return false, false, nil
return true, false, nil
}

View File

@@ -89,7 +89,6 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
// @param body body registryCreatePayload true "Registry details"
// @success 200 {object} portainer.Registry "Success"
// @failure 400 "Invalid request"
// @failure 409 "Another registry with the same name or same URL & credentials already exists"
// @failure 500 "Server error"
// @router /registries [post]
func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -26,6 +26,14 @@ import (
// @failure 500 "Server error"
// @router /registries/{id} [get]
func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
hasAccess, isAdmin, err := handler.userHasRegistryAccess(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
if !hasAccess {
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest("Invalid registry identifier route variable", err)
@@ -38,14 +46,6 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to find a registry with the specified identifier inside the database", err)
}
hasAccess, isAdmin, err := handler.userHasRegistryAccess(r, registry)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
if !hasAccess {
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}
hideFields(registry, !isAdmin)
return response.JSON(w, registry)
}

View File

@@ -52,7 +52,7 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
// @success 200 {object} portainer.Registry "Success"
// @failure 400 "Invalid request"
// @failure 404 "Registry not found"
// @failure 409 "Another registry with the same name or same URL & credentials already exists"
// @failure 409 "Another registry with the same URL already exists"
// @failure 500 "Server error"
// @router /registries/{id} [put]
func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -63,7 +63,7 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
// @param body body resourceControlCreatePayload true "Resource control details"
// @success 200 {object} portainer.ResourceControl "Success"
// @failure 400 "Invalid request"
// @failure 409 "A resource control is already associated to this resource"
// @failure 409 "Resource control already exists"
// @failure 500 "Server error"
// @router /resource_controls [post]
func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -229,7 +229,6 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
// @param body body composeStackFromGitRepositoryPayload true "stack config"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 409 "Stack name or webhook ID already exists"
// @failure 500 "Server error"
// @router /stacks/create/standalone/repository [post]
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {

View File

@@ -195,7 +195,6 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 409 "Stack name or webhook ID already exists"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/repository [post]
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {

View File

@@ -188,7 +188,6 @@ func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference
// @param body body swarmStackFromGitRepositoryPayload true "stack config"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 409 "Stack name or webhook ID already exists"
// @failure 500 "Server error"
// @router /stacks/create/swarm/repository [post]
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {

View File

@@ -46,7 +46,6 @@ func (payload *stackMigratePayload) Validate(r *http.Request) error {
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Stack not found"
// @failure 409 "A stack with the same name is already running on the target environment(endpoint)"
// @failure 500 "Server error"
// @router /stacks/{id}/migrate [post]
func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -29,7 +29,6 @@ import (
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Not found"
// @failure 409 "Stack name is not unique"
// @failure 500 "Server error"
// @router /stacks/{id}/start [post]
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -19,7 +19,7 @@ import (
// @param webhookID path string true "Stack identifier"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 409 "Autoupdate for the stack isn't available"
// @failure 409 "Conflict"
// @failure 500 "Server error"
// @router /stacks/webhooks/{webhookID} [post]
func (handler *Handler) webhookInvoke(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -35,7 +35,7 @@ func (payload *tagCreatePayload) Validate(r *http.Request) error {
// @produce json
// @param body body tagCreatePayload true "Tag details"
// @success 200 {object} portainer.Tag "Success"
// @failure 409 "This name is already associated to a tag"
// @failure 409 "Tag name exists"
// @failure 500 "Server error"
// @router /tags [post]
func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -38,7 +38,7 @@ func (payload *teamCreatePayload) Validate(r *http.Request) error {
// @param body body teamCreatePayload true "details"
// @success 200 {object} portainer.Team "Success"
// @failure 400 "Invalid request"
// @failure 409 "A team with the same name already exists"
// @failure 409 "Team already exists"
// @failure 500 "Server error"
// @router /teams [post]
func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

View File

@@ -118,9 +118,9 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
return response.JSONWithStatus(w, accessTokenResponse{rawAPIKey, *apiKey}, http.StatusCreated)
}
func (handler *Handler) usesInternalAuthentication(userID portainer.UserID) (bool, error) {
// userID 1 is the admin user and always uses internal auth
if userID == 1 {
func (handler *Handler) usesInternalAuthentication(userid portainer.UserID) (bool, error) {
// userid 1 is the admin user and always uses internal auth
if userid == 1 {
return true, nil
}

View File

@@ -45,9 +45,9 @@ func (payload *webhookCreatePayload) Validate(r *http.Request) error {
// @produce json
// @param body body webhookCreatePayload true "Webhook data"
// @success 200 {object} portainer.Webhook
// @failure 400 "Invalid request"
// @failure 409 "A webhook for this resource already exists"
// @failure 500 "Server error"
// @failure 400
// @failure 409
// @failure 500
// @router /webhooks [post]
func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload webhookCreatePayload

View File

@@ -18,12 +18,12 @@ import (
)
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
tunnelAddr, err := handler.ReverseTunnelService.TunnelAddr(params.endpoint)
tunnel, err := handler.ReverseTunnelService.GetActiveTunnel(params.endpoint)
if err != nil {
return err
}
agentURL, err := url.Parse("http://" + tunnelAddr)
agentURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
if err != nil {
return err
}
@@ -93,7 +93,7 @@ func (handler *Handler) doProxyWebsocketRequest(
}
if isEdge {
handler.ReverseTunnelService.UpdateLastActivity(params.endpoint.ID)
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
handler.ReverseTunnelService.KeepTunnelAlive(params.endpoint.ID, r.Context(), portainer.WebSocketKeepAlive)
}

View File

@@ -7,7 +7,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
requesthelpers "github.com/portainer/portainer/pkg/libhttp/request"
@@ -64,22 +63,3 @@ func FetchEndpoint(request *http.Request) (*portainer.Endpoint, error) {
return contextData.(*portainer.Endpoint), nil
}
func CheckEndpointAuthorization(bouncer security.BouncerService) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
endpoint, err := FetchEndpoint(r)
if err != nil {
httperror.WriteError(w, http.StatusNotFound, "Unable to find an environment on request context", err)
return
}
if err = bouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
httperror.WriteError(w, http.StatusForbidden, "Permission denied to access environment", err)
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -26,12 +26,12 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
urlString := endpoint.URL
if endpointutils.IsEdgeEndpoint(endpoint) {
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed starting tunnel")
}
urlString = "http://" + tunnelAddr
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.ParseURL(urlString)

View File

@@ -1,6 +1,7 @@
package factory
import (
"fmt"
"io"
"net/http"
"strings"
@@ -34,11 +35,8 @@ func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (
func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
rawURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
if err != nil {
return nil, err
}
rawURL = "http://" + tunnelAddr
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.ParseURL(rawURL)

View File

@@ -138,7 +138,9 @@ func (transport *Transport) executeDockerRequest(request *http.Request) (*http.R
}
if err == nil {
transport.reverseTunnelService.UpdateLastActivity(transport.endpoint.ID)
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
} else {
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
}
return response, err

View File

@@ -51,11 +51,8 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
}
func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
if err != nil {
return nil, err
}
rawURL := "http://" + tunnelAddr
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
rawURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
endpointURL, err := url.Parse(rawURL)
if err != nil {

View File

@@ -59,7 +59,9 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
response, err := transport.baseTransport.RoundTrip(request)
if err == nil {
transport.reverseTunnelService.UpdateLastActivity(transport.endpoint.ID)
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
} else {
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
}
return response, err

View File

@@ -528,12 +528,7 @@ func (bouncer *RequestBouncer) EdgeComputeOperation(next http.Handler) http.Hand
// - public routes
// - kubectl - a bearer token is needed, and no csrf token can be sent
// - api token
// - docker desktop extension
func ShouldSkipCSRFCheck(r *http.Request, isDockerDesktopExtension bool) (bool, error) {
if isDockerDesktopExtension {
return true, nil
}
func ShouldSkipCSRFCheck(r *http.Request) (bool, error) {
cookie, _ := r.Cookie(portainer.AuthCookieKey)
hasCookie := cookie != nil && cookie.Value != ""

View File

@@ -386,52 +386,40 @@ func Test_apiKeyLookup(t *testing.T) {
func Test_ShouldSkipCSRFCheck(t *testing.T) {
tt := []struct {
name string
cookieValue string
apiKey string
authHeader string
isDockerDesktopExtension bool
expectedResult bool
expectedError bool
name string
cookieValue string
apiKey string
authHeader string
expectedResult bool
expectedError bool
}{
{
name: "Should return false (not skip) when cookie is present",
cookieValue: "test-cookie",
isDockerDesktopExtension: false,
name: "Should return false when cookie is present",
cookieValue: "test-cookie",
},
{
name: "Should return true (skip) when cookie is present and docker desktop extension is true",
cookieValue: "test-cookie",
isDockerDesktopExtension: true,
expectedResult: true,
name: "Should return true when cookie is not present",
cookieValue: "",
expectedResult: true,
},
{
name: "Should return true (skip) when cookie is not present",
cookieValue: "",
isDockerDesktopExtension: false,
expectedResult: true,
name: "Should return true when api key is present",
cookieValue: "",
apiKey: "test-api-key",
expectedResult: true,
},
{
name: "Should return true (skip) when api key is present",
cookieValue: "",
apiKey: "test-api-key",
isDockerDesktopExtension: false,
expectedResult: true,
name: "Should return true when auth header is present",
cookieValue: "",
authHeader: "test-auth-header",
expectedResult: true,
},
{
name: "Should return true (skip) when auth header is present",
cookieValue: "",
authHeader: "test-auth-header",
isDockerDesktopExtension: false,
expectedResult: true,
},
{
name: "Should return false (not skip) and error when both api key and auth header are present",
cookieValue: "",
apiKey: "test-api-key",
authHeader: "test-auth-header",
isDockerDesktopExtension: false,
expectedError: true,
name: "Should return false and error when both api key and auth header are present",
cookieValue: "",
apiKey: "test-api-key",
authHeader: "test-auth-header",
expectedError: true,
},
}
@@ -449,7 +437,7 @@ func Test_ShouldSkipCSRFCheck(t *testing.T) {
req.Header.Set(jwtTokenHeader, test.authHeader)
}
result, err := ShouldSkipCSRFCheck(req, test.isDockerDesktopExtension)
result, err := ShouldSkipCSRFCheck(req)
is.Equal(test.expectedResult, result)
if test.expectedError {
is.Error(err)

View File

@@ -61,7 +61,6 @@ import (
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
edgestackservice "github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/internal/ssl"
"github.com/portainer/portainer/api/internal/upgrade"
k8s "github.com/portainer/portainer/api/kubernetes"
@@ -381,7 +380,9 @@ func (server *Server) Start() error {
}
go shutdown(server.ShutdownCtx, httpsServer)
go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
// Temporarily disable for EE-6905 until we have a solution for the snapshotter
// go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
return httpsServer.ListenAndServeTLS("", "")
}

View File

@@ -430,155 +430,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
}
}
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
func (service *Service) RemoveTeamAccessPolicies(tx dataservices.DataStoreTx, teamID portainer.TeamID) error {
endpoints, err := tx.Endpoint().Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyTeamID := range endpoint.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpoint.TeamAccessPolicies, policyTeamID)
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := tx.EndpointGroup().ReadAll()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyTeamID := range endpointGroup.TeamAccessPolicies {
if policyTeamID == teamID {
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
err := tx.EndpointGroup().Update(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := tx.Registry().ReadAll()
if err != nil {
return err
}
// iterate over all environments for all registries
// and evict all direct accesses to the registries the team had
// we could have built a range of the teams's environments accesses while removing them above
// but ranging over all environments (registryAccessPolicy is indexed by environmentId)
// makes sure we cleanup all resources in case an access was not removed when a team was removed from an env
for _, registry := range registries {
updateRegistry := false
for _, registryAccessPolicy := range registry.RegistryAccesses {
if _, ok := registryAccessPolicy.TeamAccessPolicies[teamID]; ok {
delete(registryAccessPolicy.TeamAccessPolicies, teamID)
updateRegistry = true
}
}
if updateRegistry {
if err := tx.Registry().Update(registry.ID, &registry); err != nil {
return err
}
}
}
return service.UpdateUsersAuthorizationsTx(tx)
}
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
func (service *Service) RemoveUserAccessPolicies(tx dataservices.DataStoreTx, userID portainer.UserID) error {
endpoints, err := tx.Endpoint().Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
for policyUserID := range endpoint.UserAccessPolicies {
if policyUserID == userID {
delete(endpoint.UserAccessPolicies, policyUserID)
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
break
}
}
}
endpointGroups, err := tx.EndpointGroup().ReadAll()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
for policyUserID := range endpointGroup.UserAccessPolicies {
if policyUserID == userID {
delete(endpointGroup.UserAccessPolicies, policyUserID)
err := tx.EndpointGroup().Update(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
break
}
}
}
registries, err := tx.Registry().ReadAll()
if err != nil {
return err
}
// iterate over all environments for all registries
// and evict all direct accesses to the registries the user had
// we could have built a range of the user's environments accesses while removing them above
// but ranging over all environments (registryAccessPolicy is indexed by environmentId)
// makes sure we cleanup all resources in case an access was not removed when a user was removed from an env
for _, registry := range registries {
updateRegistry := false
for _, registryAccessPolicy := range registry.RegistryAccesses {
if _, ok := registryAccessPolicy.UserAccessPolicies[userID]; ok {
delete(registryAccessPolicy.UserAccessPolicies, userID)
updateRegistry = true
}
}
if updateRegistry {
if err := tx.Registry().Update(registry.ID, &registry); err != nil {
return err
}
}
}
return nil
}
// UpdateUserAuthorizations will update the authorizations for the provided userid
func (service *Service) UpdateUserAuthorizations(tx dataservices.DataStoreTx, userID portainer.UserID) error {
err := service.updateUserAuthorizations(tx, userID)
if err != nil {
return err
}
return nil
}
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
func (service *Service) UpdateUsersAuthorizations() error {
return service.UpdateUsersAuthorizationsTx(service.dataStore)

View File

@@ -6,7 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/unique"
)
var ErrEdgeGroupNotFound = errors.New("edge group was not found")
@@ -33,7 +32,7 @@ func EdgeStackRelatedEndpoints(edgeGroupIDs []portainer.EdgeGroupID, endpoints [
edgeStackEndpoints = append(edgeStackEndpoints, EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)...)
}
return unique.Unique(edgeStackEndpoints), nil
return edgeStackEndpoints, nil
}
type EndpointRelationsConfig struct {

View File

@@ -1,9 +1,6 @@
package edge
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
import portainer "github.com/portainer/portainer/api"
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Environment(Endpoint)
func EndpointRelatedEdgeStacks(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) []portainer.EdgeStackID {
@@ -27,15 +24,3 @@ func EndpointRelatedEdgeStacks(endpoint *portainer.Endpoint, endpointGroup *port
return relatedEdgeStacks
}
func EffectiveCheckinInterval(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint) int {
if endpoint.EdgeCheckinInterval != 0 {
return endpoint.EdgeCheckinInterval
}
if settings, err := tx.Settings().Settings(); err == nil {
return settings.EdgeAgentCheckinInterval
}
return portainer.DefaultEdgeAgentCheckinIntervalInSeconds
}

View File

@@ -79,26 +79,21 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
if endpoint.Kubernetes.Flags.IsServerIngressClassDetected {
return
}
defer func() {
endpoint.Kubernetes.Flags.IsServerIngressClassDetected = true
if err := endpointService.UpdateEndpoint(endpoint.ID, endpoint); err != nil {
log.Debug().Err(err).Msg("unable to store found IngressClasses inside the database")
}
endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
}()
cli, err := factory.GetKubeClient(endpoint)
if err != nil {
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
return
}
controllers, err := cli.GetIngressControllers()
if err != nil {
log.Debug().Err(err).Msg("failed to fetch ingressclasses")
return
}
@@ -111,68 +106,69 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
}
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
err = endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
if err != nil {
log.Debug().Err(err).Msg("unable to store found IngressClasses inside the database")
return
}
}
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
if endpoint.Kubernetes.Flags.IsServerMetricsDetected {
return
}
defer func() {
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
if err := endpointService.UpdateEndpoint(endpoint.ID, endpoint); err != nil {
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
}
endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
}()
cli, err := factory.GetKubeClient(endpoint)
if err != nil {
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
return
}
if _, err := cli.GetMetrics(); err != nil {
_, err = cli.GetMetrics()
if err != nil {
log.Debug().Err(err).Msg("unable to fetch metrics: leaving metrics collection disabled.")
return
}
endpoint.Kubernetes.Configuration.UseServerMetrics = true
if err != nil {
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
return
}
}
func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) error {
if endpoint.Kubernetes.Flags.IsServerStorageDetected {
return nil
}
defer func() {
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
if err := endpointService.UpdateEndpoint(endpoint.ID, endpoint); err != nil {
log.Info().Err(err).Msg("unable to enable storage class inside the database")
}
}()
cli, err := factory.GetKubeClient(endpoint)
if err != nil {
log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection")
return err
}
storage, err := cli.GetStorage()
if err != nil {
log.Debug().Err(err).Msg("unable to fetch storage classes: leaving storage classes disabled")
return err
} else if len(storage) == 0 {
}
if len(storage) == 0 {
log.Info().Err(err).Msg("zero storage classes found: they may be still building, retrying in 30 seconds")
return fmt.Errorf("zero storage classes found: they may be still building, retrying in 30 seconds")
}
endpoint.Kubernetes.Configuration.StorageClasses = storage
err = endpointService.UpdateEndpoint(
portainer.EndpointID(endpoint.ID),
endpoint,
)
if err != nil {
log.Debug().Err(err).Msg("unable to enable storage class inside the database")
return err
}
return nil
}

View File

@@ -57,6 +57,8 @@ func NewService(
// NewBackgroundSnapshotter queues snapshots of existing edge environments that
// do not have one already
func NewBackgroundSnapshotter(dataStore dataservices.DataStore, tunnelService portainer.ReverseTunnelService) {
var endpointIDs []portainer.EndpointID
err := dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
endpoints, err := tx.Endpoint().Endpoints()
if err != nil {
@@ -64,16 +66,14 @@ func NewBackgroundSnapshotter(dataStore dataservices.DataStore, tunnelService po
}
for _, e := range endpoints {
if !endpointutils.IsEdgeEndpoint(&e) || e.Edge.AsyncMode || !e.UserTrusted {
if !endpointutils.IsEdgeEndpoint(&e) {
continue
}
s, err := tx.Snapshot().Read(e.ID)
if dataservices.IsErrObjectNotFound(err) ||
(err == nil && s.Docker == nil && s.Kubernetes == nil) {
if err := tunnelService.Open(&e); err != nil {
log.Error().Err(err).Msg("could not open the tunnel")
}
endpointIDs = append(endpointIDs, e.ID)
}
}
@@ -83,6 +83,11 @@ func NewBackgroundSnapshotter(dataStore dataservices.DataStore, tunnelService po
log.Error().Err(err).Msg("background snapshotter failure")
return
}
for _, endpointID := range endpointIDs {
tunnelService.SetTunnelStatusToActive(endpointID)
time.Sleep(10 * time.Second)
}
}
func parseSnapshotFrequency(snapshotInterval string, dataStore dataservices.DataStore) (float64, error) {
@@ -307,7 +312,10 @@ func updateEndpointStatus(tx dataservices.DataStoreTx, endpoint *portainer.Endpo
// Run the pending actions
if latestEndpointReference.Status == portainer.EndpointStatusUp {
pendingActionsService.Execute(endpoint.ID)
err = pendingActionsService.Execute(endpoint.ID)
if err != nil {
log.Error().Err(err).Msg("background schedule error (environment snapshot), unable to execute pending actions")
}
}
}

View File

@@ -4,7 +4,6 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/dataservices/errors"
)
@@ -35,7 +34,6 @@ type testDatastore struct {
version dataservices.VersionService
webhook dataservices.WebhookService
pendingActionsService dataservices.PendingActionsService
connection portainer.Connection
}
func (d *testDatastore) Backup(path string) (string, error) { return "", nil }
@@ -90,10 +88,6 @@ func (d *testDatastore) PendingActions() dataservices.PendingActionsService {
return d.pendingActionsService
}
func (d *testDatastore) Connection() portainer.Connection {
return d.connection
}
func (d *testDatastore) IsErrObjectNotFound(e error) bool {
return false
}
@@ -111,8 +105,7 @@ type datastoreOption = func(d *testDatastore)
// NewDatastore creates new instance of testDatastore.
// Will apply options before returning, opts will be applied from left to right.
func NewDatastore(options ...datastoreOption) *testDatastore {
conn, _ := database.NewDatabase("boltdb", "", nil)
d := testDatastore{connection: conn}
d := testDatastore{}
for _, o := range options {
o(&d)
}

View File

@@ -240,11 +240,11 @@ func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*r
}
func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
tunnelAddr, err := factory.reverseTunnelService.TunnelAddr(endpoint)
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed activating tunnel")
}
endpointURL := fmt.Sprintf("http://%s/kubernetes", tunnelAddr)
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
config, err := clientcmd.BuildConfigFromFlags(endpointURL, "")
if err != nil {

View File

@@ -8,7 +8,6 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
@@ -65,8 +64,8 @@ func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, er
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
portainerLabels := map[string]string{
"io.portainer.kubernetes.resourcepool.name": stackutils.SanitizeLabel(info.Name),
"io.portainer.kubernetes.resourcepool.owner": stackutils.SanitizeLabel(info.Owner),
"io.portainer.kubernetes.resourcepool.name": info.Name,
"io.portainer.kubernetes.resourcepool.owner": info.Owner,
}
var ns v1.Namespace

View File

@@ -4,11 +4,11 @@ import (
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/stacks/stackutils"
"gopkg.in/yaml.v3"
)
@@ -28,13 +28,19 @@ type KubeAppLabels struct {
Kind string
}
// convert string to valid kubernetes label by replacing invalid characters with periods
func sanitizeLabel(value string) string {
re := regexp.MustCompile(`[^A-Za-z0-9\.\-\_]+`)
return re.ReplaceAllString(value, ".")
}
// ToMap converts KubeAppLabels to a map[string]string
func (kal *KubeAppLabels) ToMap() map[string]string {
return map[string]string{
labelPortainerAppStackID: strconv.Itoa(kal.StackID),
labelPortainerAppStack: stackutils.SanitizeLabel(kal.StackName),
labelPortainerAppName: stackutils.SanitizeLabel(kal.StackName),
labelPortainerAppOwner: stackutils.SanitizeLabel(kal.Owner),
labelPortainerAppStack: kal.StackName,
labelPortainerAppName: kal.StackName,
labelPortainerAppOwner: sanitizeLabel(kal.Owner),
labelPortainerAppKind: kal.Kind,
}
}
@@ -43,7 +49,7 @@ func (kal *KubeAppLabels) ToMap() map[string]string {
func GetHelmAppLabels(name, owner string) map[string]string {
return map[string]string{
labelPortainerAppName: name,
labelPortainerAppOwner: stackutils.SanitizeLabel(owner),
labelPortainerAppOwner: sanitizeLabel(owner),
}
}

View File

@@ -55,19 +55,14 @@ func (service *PendingActionsService) Create(r portainer.PendingActions) error {
return service.dataStore.PendingActions().Create(&r)
}
func (service *PendingActionsService) Execute(id portainer.EndpointID) {
// Run in a goroutine to avoid blocking the main thread due to db tx =
go service.execute(id)
}
func (service *PendingActionsService) Execute(id portainer.EndpointID) error {
func (service *PendingActionsService) execute(environmentID portainer.EndpointID) {
service.mu.Lock()
defer service.mu.Unlock()
endpoint, err := service.dataStore.Endpoint().Endpoint(environmentID)
endpoint, err := service.dataStore.Endpoint().Endpoint(id)
if err != nil {
log.Debug().Msgf("failed to retrieve environment %d: %v", environmentID, err)
return
return fmt.Errorf("failed to retrieve environment %d: %w", id, err)
}
isKubernetesEndpoint := endpointutils.IsKubernetesEndpoint(endpoint) && !endpointutils.IsEdgeEndpoint(endpoint)
@@ -75,16 +70,16 @@ func (service *PendingActionsService) execute(environmentID portainer.EndpointID
// EndpointStatusUp is only relevant for non-Kubernetes endpoints
// Sometimes the endpoint is UP but the status is not updated in the database
if !isKubernetesEndpoint && endpoint.Status != portainer.EndpointStatusUp {
log.Debug().Msgf("failed to create Kubernetes client for environment %d: %v", environmentID, err)
return
log.Debug().Msgf("Environment %q (id: %d) is not up", endpoint.Name, id)
return fmt.Errorf("environment %q (id: %d) is not up", endpoint.Name, id)
}
// For Kubernetes endpoints, we need to check if the endpoint is up by creating a kube client
if isKubernetesEndpoint {
if client, _ := service.kubeFactory.GetKubeClient(endpoint); client != nil {
if _, err = client.ServerVersion(); err != nil {
log.Debug().Err(err).Msgf("Environment %q (id: %d) is not up", endpoint.Name, environmentID)
return
log.Debug().Err(err).Msgf("Environment %q (id: %d) is not up", endpoint.Name, id)
return fmt.Errorf("environment %q (id: %d) is not up", endpoint.Name, id)
}
}
}
@@ -92,33 +87,26 @@ func (service *PendingActionsService) execute(environmentID portainer.EndpointID
pendingActions, err := service.dataStore.PendingActions().ReadAll()
if err != nil {
log.Error().Err(err).Msgf("failed to retrieve pending actions")
return
return fmt.Errorf("failed to retrieve pending actions for environment %d: %w", id, err)
}
if len(pendingActions) > 0 {
log.Debug().Msgf("Found %d pending actions", len(pendingActions))
}
for i, endpointPendingAction := range pendingActions {
if endpointPendingAction.EndpointID == environmentID {
if i == 0 {
// We have at least 1 pending action for this environment
log.Debug().Msgf("Executing pending actions for environment %d", environmentID)
}
for _, endpointPendingAction := range pendingActions {
if endpointPendingAction.EndpointID == id {
err := service.executePendingAction(endpointPendingAction, endpoint)
if err != nil {
log.Warn().Err(err).Msgf("failed to execute pending action")
continue
return fmt.Errorf("failed to execute pending action: %w", err)
}
err = service.dataStore.PendingActions().Delete(endpointPendingAction.ID)
if err != nil {
log.Error().Err(err).Msgf("failed to delete pending action")
continue
return fmt.Errorf("failed to delete pending action: %w", err)
}
}
}
return nil
}
func (service *PendingActionsService) executePendingAction(pendingAction portainer.PendingActions, endpoint *portainer.Endpoint) error {

View File

@@ -1306,6 +1306,7 @@ type (
Status string
LastActivity time.Time
Port int
Jobs []EdgeJob
Credentials string
}
@@ -1564,13 +1565,13 @@ type (
ReverseTunnelService interface {
StartTunnelServer(addr, port string, snapshotService SnapshotService) error
StopTunnelServer() error
GenerateEdgeKey(apiURL, tunnelAddr string, endpointIdentifier int) string
Open(endpoint *Endpoint) error
Config(endpointID EndpointID) TunnelDetails
TunnelAddr(endpoint *Endpoint) (string, error)
UpdateLastActivity(endpointID EndpointID)
GenerateEdgeKey(url, host string, endpointIdentifier int) string
SetTunnelStatusToActive(endpointID EndpointID)
SetTunnelStatusToRequired(endpointID EndpointID) error
SetTunnelStatusToIdle(endpointID EndpointID)
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
EdgeJobs(endpointId EndpointID) []EdgeJob
GetTunnelDetails(endpointID EndpointID) TunnelDetails
GetActiveTunnel(endpoint *Endpoint) (TunnelDetails, error)
AddEdgeJob(endpoint *Endpoint, edgeJob *EdgeJob)
RemoveEdgeJob(edgeJobID EdgeJobID)
RemoveEdgeJobFromEndpoint(endpointID EndpointID, edgeJobID EdgeJobID)
@@ -1601,7 +1602,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.21.0-rc2"
APIVersion = "2.20.3"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
@@ -1885,6 +1886,8 @@ const (
EdgeAgentIdle string = "IDLE"
// EdgeAgentManagementRequired represents a required state for a tunnel connected to an Edge environment(endpoint)
EdgeAgentManagementRequired string = "REQUIRED"
// EdgeAgentActive represents an active state for a tunnel connected to an Edge environment(endpoint)
EdgeAgentActive string = "ACTIVE"
)
// represents an authorization type

View File

@@ -31,7 +31,6 @@ const (
type unpackerCmdBuilderOptions struct {
pullImage bool
prune bool
forceRecreate bool
composeDestination string
registries []portainer.Registry
}
@@ -63,13 +62,12 @@ func (d *stackDeployer) buildUnpackerCmdForStack(stack *portainer.Stack, operati
return fn(stack, opts, registriesStrings, envStrings), nil
}
// deploy [-u username -p password] [--skip-tls-verify] [--force-recreate] [-k] [--env KEY1=VALUE1 --env KEY2=VALUE2] <git-repo-url> <ref> <project-name> <destination> <compose-file-path> [<more-file-paths>...]
// deploy [-u username -p password] [--skip-tls-verify] [--env KEY1=VALUE1 --env KEY2=VALUE2] <git-repo-url> <ref> <project-name> <destination> <compose-file-path> [<more-file-paths>...]
func buildDeployCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions, registries []string, env []string) []string {
cmd := []string{}
cmd = append(cmd, UnpackerCmdDeploy)
cmd = appendGitAuthIfNeeded(cmd, stack)
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
cmd = appendForceRecreateIfNeeded(cmd, opts.forceRecreate)
cmd = append(cmd, env...)
cmd = append(cmd, registries...)
cmd = append(cmd, stack.GitConfig.URL)
@@ -126,13 +124,12 @@ func buildComposeStopCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions,
return cmd
}
// swarm-deploy [-u username -p password] [--skip-tls-verify] [--force-recreate] [-f] [-r] [-k] [--env KEY1=VALUE1 --env KEY2=VALUE2] <git-repo-url> <git-ref> <project-name> <destination> <compose-file-path> [<more-file-paths>...]
// swarm-deploy [-u username -p password] [--skip-tls-verify] [-f] [-r] [--env KEY1=VALUE1 --env KEY2=VALUE2] <git-repo-url> <git-ref> <project-name> <destination> <compose-file-path> [<more-file-paths>...]
func buildSwarmDeployCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions, registries []string, env []string) []string {
cmd := []string{}
cmd = append(cmd, UnpackerCmdSwarmDeploy)
cmd = appendGitAuthIfNeeded(cmd, stack)
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
cmd = appendForceRecreateIfNeeded(cmd, opts.forceRecreate)
if opts.pullImage {
cmd = append(cmd, "-f")
}
@@ -206,13 +203,6 @@ func appendAdditionalFiles(cmd []string, files []string) []string {
return cmd
}
func appendForceRecreateIfNeeded(cmd []string, forceRecreate bool) []string {
if forceRecreate {
cmd = append(cmd, "--force-recreate")
}
return cmd
}
func getRegistry(registries []portainer.Registry, dataStore dataservices.DataStore) []string {
cmds := []string{}

View File

@@ -70,8 +70,7 @@ func (d *stackDeployer) DeployRemoteComposeStack(
endpoint,
OperationDeploy,
unpackerCmdBuilderOptions{
forceRecreate: forceRecreate,
registries: registries,
registries: registries,
},
)
}
@@ -120,10 +119,10 @@ func (d *stackDeployer) DeployRemoteSwarmStack(
defer d.swarmStackManager.Logout(endpoint)
return d.remoteStack(stack, endpoint, OperationSwarmDeploy, unpackerCmdBuilderOptions{
pullImage: pullImage,
prune: prune,
forceRecreate: stack.AutoUpdate != nil && stack.AutoUpdate.ForceUpdate,
registries: registries,
pullImage: pullImage,
prune: prune,
registries: registries,
})
}

View File

@@ -3,7 +3,6 @@ package stackutils
import (
"fmt"
"regexp"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
@@ -38,11 +37,10 @@ func ResourceControlID(endpointID portainer.EndpointID, name string) string {
return fmt.Sprintf("%d_%s", endpointID, name)
}
// convert string to valid kubernetes label by replacing invalid characters with periods and removing any periods at the beginning or end of the string
// convert string to valid kubernetes label by replacing invalid characters with periods
func SanitizeLabel(value string) string {
re := regexp.MustCompile(`[^A-Za-z0-9\.\-\_]+`)
onlyAllowedCharacterString := re.ReplaceAllString(value, ".")
return strings.Trim(onlyAllowedCharacterString, ".-_")
return re.ReplaceAllString(value, ".")
}
// IsGitStack checks if the stack is a git stack or not

View File

@@ -16,7 +16,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
parent: 'endpoint',
url: '/docker',
abstract: true,
onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService, EndpointProvider) {
onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService) {
return $async(async () => {
const dockerTypes = [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment];
@@ -44,11 +44,9 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'docker.dashboard' };
} else {
EndpointProvider.clean();
Notifications.error('Failed loading environment', e);
}
$state.go('portainer.home', params, { reload: true, inherit: false });
return false;
}
async function checkEndpointStatus(endpoint) {

View File

@@ -15,12 +15,9 @@
<td>{{ device.Name }}</td>
<td>{{ device.Vendor }}</td>
</tr>
<tr ng-if="$ctrl.devices === undefined">
<tr ng-if="!$ctrl.devices">
<td colspan="2" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.devices === null">
<td colspan="2" class="text-muted text-center"> Failed to load devices. </td>
</tr>
<tr ng-if="$ctrl.devices.length === 0">
<td colspan="2" class="text-muted text-center"> No device available. </td>
</tr>

View File

@@ -15,12 +15,9 @@
<td>{{ disk.Vendor }}</td>
<td>{{ disk.Size | humansize }}</td>
</tr>
<tr ng-if="$ctrl.disks === undefined">
<tr ng-if="!$ctrl.disks">
<td colspan="2" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.disks === null">
<td colspan="2" class="text-muted text-center"> Failed to load devices. </td>
</tr>
<tr ng-if="$ctrl.disks.length === 0">
<td colspan="2" class="text-muted text-center"> No disks available. </td>
</tr>

View File

@@ -41,7 +41,7 @@
class="form-control"
ng-model="$ctrl.data.mountPoint"
name="nfs_mountpoint"
placeholder="e.g. /export/share, :/export/share, address:/export/share, /share, :/share or address:/share"
placeholder="e.g. /export/share, :/export/share, /share or :/share"
required
/>
</div>

View File

@@ -70,6 +70,22 @@ angular.module('portainer.docker').factory('NetworkService', [
return Network.remove({ id: id }).$promise;
};
service.disconnectContainer = function (networkId, containerId, force) {
return Network.disconnect({ id: networkId }, { Container: containerId, Force: force }).$promise;
};
service.connectContainer = function (networkId, containerId, aliases) {
var payload = {
Container: containerId,
};
if (aliases) {
payload.EndpointConfig = {
Aliases: aliases,
};
}
return Network.connect({ id: networkId }, payload).$promise;
};
return service;
},
]);

View File

@@ -121,8 +121,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
.map((k) => k + '=' + params[k])
.join('&');
const isLinuxCommand = execConfig.Cmd ? isLinuxTerminalCommand(execConfig.Cmd[0]) : false;
initTerm(url, ExecService.resizeTTY.bind(this, params.id), isLinuxCommand);
initTerm(url, ExecService.resizeTTY.bind(this, params.id));
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to exec into container');
@@ -166,12 +165,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
restcall(termWidth + add, termHeight + add, 1);
}
function isLinuxTerminalCommand(command) {
const validShellCommands = ['ash', 'bash', 'dash', 'sh'];
return validShellCommands.includes(command);
}
function initTerm(url, resizeRestCall, isLinuxTerm = false) {
function initTerm(url, resizeRestCall) {
let resizefun = resize.bind(this, resizeRestCall);
if ($transition$.params().nodeName) {
@@ -189,20 +183,13 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
socket.onopen = function () {
$scope.state = states.connected;
term = new Terminal();
if (isLinuxTerm) {
// linux terminals support xterm
socket.send('export LANG=C.UTF-8\n');
socket.send('export LC_ALL=C.UTF-8\n');
socket.send('export TERM="xterm-256color"\n');
socket.send('alias ls="ls --color=auto"\n');
socket.send('echo -e "\\033[2J\\033[H"\n');
}
socket.send('export LANG=C.UTF-8\n');
socket.send('export LC_ALL=C.UTF-8\n');
socket.send('clear\n');
term.onData(function (data) {
socket.send(data);
});
var terminal_container = document.getElementById('terminal-container');
term.open(terminal_container);
term.focus();
@@ -220,13 +207,11 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
socket.onmessage = function (e) {
term.write(e.data);
};
socket.onerror = function (err) {
$scope.disconnect();
Notifications.error('Failure', err, 'Connection error');
$scope.$apply();
Notifications.error('Failure', err, 'Connection error');
};
socket.onclose = function () {
$scope.disconnect();
$scope.$apply();

View File

@@ -29,7 +29,6 @@
<select class="form-control" ng-model="formValues.command" id="command">
<option value="ash" ng-if="imageOS == 'linux'">/bin/ash</option>
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="dash" ng-if="imageOS == 'linux'">/bin/dash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>

View File

@@ -349,5 +349,15 @@
</div>
</div>
<docker-container-networks-datatable ng-if="container.NetworkSettings.Networks" dataset="container.NetworkSettings.Networks" container="container" node-name="nodeName">
<docker-container-networks-datatable
ng-if="container.NetworkSettings.Networks"
dataset="container.NetworkSettings.Networks"
container="container"
available-networks="availableNetworks"
on-join="(containerJoinNetwork)"
join-in-progress="state.joinNetworkInProgress"
on-leave="(containerLeaveNetwork)"
leave-in-progress="state.leaveNetworkInProgress"
node-name="nodeName"
>
</docker-container-networks-datatable>

View File

@@ -14,13 +14,37 @@ angular.module('portainer.docker').controller('ContainerController', [
'$filter',
'$async',
'Commit',
'ContainerHelper',
'ContainerService',
'ImageHelper',
'NetworkService',
'Notifications',
'ResourceControlService',
'RegistryService',
'ImageService',
'HttpRequestHelper',
'Authentication',
'endpoint',
function ($q, $scope, $state, $transition$, $filter, $async, Commit, ContainerService, ImageHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
function (
$q,
$scope,
$state,
$transition$,
$filter,
$async,
Commit,
ContainerHelper,
ContainerService,
ImageHelper,
NetworkService,
Notifications,
ResourceControlService,
RegistryService,
ImageService,
HttpRequestHelper,
Authentication,
endpoint
) {
$scope.resourceType = ResourceControlType.Container;
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
@@ -37,6 +61,8 @@ angular.module('portainer.docker').controller('ContainerController', [
$scope.state = {
recreateContainerInProgress: false,
joinNetworkInProgress: false,
leaveNetworkInProgress: false,
pullImageValidity: false,
};
@@ -199,6 +225,36 @@ angular.module('portainer.docker').controller('ContainerController', [
});
};
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
$scope.state.leaveNetworkInProgress = true;
NetworkService.disconnectContainer(networkId, container.Id, false)
.then(function success() {
Notifications.success('Container left network', container.Id);
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to disconnect container from network');
})
.finally(function final() {
$scope.state.leaveNetworkInProgress = false;
});
};
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
$scope.state.joinNetworkInProgress = true;
NetworkService.connectContainer(networkId, container.Id)
.then(function success() {
Notifications.success('Container joined network', container.Id);
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect container to network');
})
.finally(function final() {
$scope.state.joinNetworkInProgress = false;
});
};
async function commitContainerAsync() {
$scope.config.commitInProgress = true;
const registryModel = $scope.config.RegistryModel;
@@ -293,6 +349,17 @@ angular.module('portainer.docker').controller('ContainerController', [
}
}
var provider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25)
.then(function success(data) {
var networks = data;
$scope.availableNetworks = networks;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve networks');
});
update();
},
]);

View File

@@ -6,14 +6,10 @@
<rd-widget-body>
<form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title"> Host and filesystem </div>
<div class="form-group">
<div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small vertical-center">
<pr-icon icon="'info'" mode="'primary'" class-name="'space-right'"></pr-icon>
<span class="text-muted"
>The environment must be <a href="https://docs.portainer.io/start/agent">running the Portainer Agent</a> to use this functionality, and the root of the host must be
bind-mounted to <b>/host</b> in the agent deployment. Check
<a href="https://docs.portainer.io/user/docker/host/setup#enable-host-management-features">our documentation</a> for more information.</span
>
These features are only available for an Agent enabled environments.
</span>
</div>
<div class="form-group">

View File

@@ -17,7 +17,7 @@ angular.module('portainer.docker').controller('HostViewController', [
this.engineDetails = {};
this.hostDetails = {};
this.devices = undefined;
this.devices = null;
this.disks = null;
function initView() {

View File

@@ -3,7 +3,6 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { confirmImageExport } from '@/react/docker/images/common/ConfirmExportModal';
import { confirmDestructive } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
angular.module('portainer.docker').controller('ImagesController', [
'$scope',
@@ -158,20 +157,24 @@ angular.module('portainer.docker').controller('ImagesController', [
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} selectedItems
* @param {boolean} force
*/
async function removeAction(selectedItems, force) {
async function doRemove(image) {
function removeAction(selectedItems, force) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (image) {
HttpRequestHelper.setPortainerAgentTargetHeader(image.nodeName);
return ImageService.deleteImage(image.id, force)
ImageService.deleteImage(image.id, force)
.then(function success() {
Notifications.success('Image successfully removed', image.id);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
}
await processItemsInBatches(selectedItems, doRemove);
$state.reload();
});
}
$scope.setPullImageValidity = setPullImageValidity;

View File

@@ -1,7 +1,6 @@
import _ from 'lodash-es';
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { confirmDelete } from '@@/modals/confirm';
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
angular.module('portainer.docker').controller('NetworksController', [
'$q',
@@ -18,10 +17,10 @@ angular.module('portainer.docker').controller('NetworksController', [
if (!confirmed) {
return null;
}
async function doRemove(network) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (network) {
HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName);
return NetworkService.remove(network.Id)
NetworkService.remove(network.Id)
.then(function success() {
Notifications.success('Network successfully removed', network.Name);
var index = $scope.networks.indexOf(network);
@@ -29,11 +28,14 @@ angular.module('portainer.docker').controller('NetworksController', [
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove network');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
}
await processItemsInBatches(selectedItems, doRemove);
$state.reload();
});
};
$scope.getNetworks = getNetworks;

View File

@@ -1,6 +1,4 @@
import { confirmDelete } from '@@/modals/confirm';
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
angular.module('portainer.docker').controller('SecretsController', [
'$scope',
'$state',
@@ -12,9 +10,9 @@ angular.module('portainer.docker').controller('SecretsController', [
if (!confirmed) {
return null;
}
async function doRemove(secret) {
return SecretService.remove(secret.Id)
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (secret) {
SecretService.remove(secret.Id)
.then(function success() {
Notifications.success('Secret successfully removed', secret.Name);
var index = $scope.secrets.indexOf(secret);
@@ -22,11 +20,14 @@ angular.module('portainer.docker').controller('SecretsController', [
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove secret');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
}
await processItemsInBatches(selectedItems, doRemove);
$state.reload();
});
};
$scope.getSecrets = getSecrets;

View File

@@ -99,7 +99,7 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
}
driverOptions.push({ name: 'o', value: options });
var mountPoint = data.mountPoint.indexOf(':') === -1 ? ':' + data.mountPoint : data.mountPoint;
var mountPoint = data.mountPoint[0] === ':' ? data.mountPoint : ':' + data.mountPoint;
driverOptions.push({ name: 'device', value: mountPoint });
}

View File

@@ -1,7 +1,5 @@
import { confirmDelete } from '@@/modals/confirm';
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
angular.module('portainer.docker').controller('VolumesController', [
'$q',
'$scope',
@@ -15,23 +13,27 @@ angular.module('portainer.docker').controller('VolumesController', [
'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
$scope.removeAction = function (selectedItems) {
confirmDelete('Do you want to remove the selected volume(s)?').then(async (confirmed) => {
async function doRemove(volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
return VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
}
confirmDelete('Do you want to remove the selected volume(s)?').then((confirmed) => {
if (confirmed) {
await processItemsInBatches(selectedItems, doRemove);
$state.reload();
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
});
}
});
};

View File

@@ -11,8 +11,7 @@
<meta name="robots" content="noindex" />
<base id="base" />
<script>
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
if (window.origin == 'http://localhost:49000') {
if (window.origin == 'file://') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:49000/';

View File

@@ -63,13 +63,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
$state,
endpoint,
KubernetesHealthService,
KubernetesNamespaceService,
Notifications,
StateManager,
$http,
Authentication,
UserService,
EndpointService,
EndpointProvider
UserService
) {
return $async(async () => {
// if the user wants to use front end cache for performance, set the angular caching settings
@@ -94,57 +93,39 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
$state.go('portainer.home');
return;
}
try {
const status = await checkEndpointStatus(
endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
? KubernetesHealthService.ping(endpoint.Id)
: // use selfsubject access review to check if we can connect to the kubernetes environment
// because it gets a fast response, and is accessible to all users
getSelfSubjectAccessReview(endpoint.Id, 'default')
);
if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
await updateEndpointStatus(endpoint, status);
}
endpoint.Status = status;
if (endpoint.Status === EnvironmentStatus.Down) {
throw new Error(
endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
? 'Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.'
: `The environment named ${endpoint.Name} is unreachable.`
);
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
//edge
try {
await KubernetesHealthService.ping(endpoint.Id);
endpoint.Status = EnvironmentStatus.Up;
} catch (e) {
endpoint.Status = EnvironmentStatus.Down;
}
}
await StateManager.updateEndpointState(endpoint);
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
}
// use selfsubject access review to check if we can connect to the kubernetes environment
// because it's gets a fast response, and is accessible to all users
try {
await getSelfSubjectAccessReview(endpoint.Id, 'default');
} catch (e) {
throw new Error(`The environment named ${endpoint.Name} is unreachable.`);
}
} catch (e) {
let params = {};
if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'kubernetes.dashboard' };
} else {
EndpointProvider.clean();
Notifications.error('Failed loading environment', e);
}
$state.go('portainer.home', params, { reload: true, inherit: false });
return false;
}
async function checkEndpointStatus(promise) {
try {
await promise;
return EnvironmentStatus.Up;
} catch (e) {
return EnvironmentStatus.Down;
}
}
async function updateEndpointStatus(endpoint, status) {
if (endpoint.Status === status) {
return;
}
await EndpointService.updateEndpoint(endpoint.Id, { Status: status });
}
});
},

View File

@@ -4,8 +4,6 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
import { getSchemeFromPort } from '@/react/common/network-utils';
angular.module('portainer.kubernetes').controller('KubernetesApplicationsDatatableController', [
'$scope',
'$controller',
@@ -107,10 +105,7 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsDatatab
// Map all load balancer service ports to ip address
let loadBalancerURLs = [];
if (item.LoadBalancerIPAddress) {
loadBalancerURLs = item.PublishedPorts.map((pp) => {
const scheme = getSchemeFromPort(pp.Port);
return `${scheme}://${item.LoadBalancerIPAddress}:${pp.Port}`;
});
loadBalancerURLs = item.PublishedPorts.map((pp) => `http://${item.LoadBalancerIPAddress}:${pp.Port}`);
}
// combine ingress urls

View File

@@ -70,7 +70,7 @@
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</th>
<th ng-if="!$ctrl.deploymentOptions.hideStacksFunctionality">
<th>
<table-column-header
col-title="'Stack'"
can-sort="true"
@@ -126,7 +126,7 @@
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span style="margin-left: 5px" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td>
<td ng-if="!$ctrl.deploymentOptions.hideStacksFunctionality">{{ item.StackName || '-' }}</td>
<td>{{ item.StackName || '-' }}</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
</td>

View File

@@ -1,14 +1,12 @@
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models/appConstants';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [
'$scope',
'$controller',
'$async',
'DatatableService',
function ($scope, $controller, $async, DatatableService) {
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.isSystemNamespace = function (item) {
@@ -20,41 +18,37 @@ angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatab
};
this.$onInit = function () {
return $async(async () => {
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.deploymentOptions = await getDeploymentOptions();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
});
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View File

@@ -68,9 +68,9 @@
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</th>
<th ng-if="!$ctrl.deploymentOptions.hideStacksFunctionality">
<th>
<table-column-header
col-title="'Stack'"
col-title="'StackName'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'StackName'"
is-sorted-desc="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"
@@ -114,7 +114,7 @@
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span style="margin-left: 5px" class="label label-primary image-tag" ng-if="$ctrl.isExternalApplication(item)">external</span>
</td>
<td ng-if="!$ctrl.deploymentOptions.hideStacksFunctionality">{{ item.StackName || '-' }}</td>
<td>{{ item.StackName || '-' }}</td>
<td title="{{ item.Image }}"
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>

View File

@@ -1,12 +1,10 @@
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
angular.module('portainer.docker').controller('KubernetesResourcePoolApplicationsDatatableController', [
'$scope',
'$controller',
'$async',
'DatatableService',
function ($scope, $controller, $async, DatatableService) {
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.isExternalApplication = function (item) {
@@ -14,40 +12,36 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolApplication
};
this.$onInit = function () {
return $async(async () => {
this.setDefaults();
this.prepareTableFromDataset();
this.setDefaults();
this.prepareTableFromDataset();
this.deploymentOptions = await getDeploymentOptions();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
});
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View File

@@ -142,14 +142,7 @@ export const ngModule = angular
),
{ stackName: 'setStackName' }
),
[
'setStackName',
'stackName',
'stacks',
'inputClassName',
'textTip',
'error',
]
['setStackName', 'stackName', 'stacks', 'inputClassName', 'textTip']
)
)
.component(

View File

@@ -172,7 +172,6 @@
text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'"
stacks="ctrl.stacks"
input-class-name="'col-lg-10 col-sm-9'"
error="ctrl.state.stackNameError"
></kube-stack-name>
<!-- #endregion -->
@@ -235,7 +234,6 @@
text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'"
stacks="ctrl.stacks"
input-class-name="'col-lg-10 col-sm-9'"
error="ctrl.state.stackNameError"
></kube-stack-name>
<!-- #endregion -->

View File

@@ -28,7 +28,6 @@ import { confirmUpdateAppIngress } from '@/react/kubernetes/applications/CreateV
import { confirm, confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { ModalType } from '@@/modals';
import { KUBE_STACK_NAME_VALIDATION_REGEX } from '@/react/kubernetes/DeployView/StackName/constants';
class KubernetesCreateApplicationController {
/* #region CONSTRUCTOR */
@@ -128,7 +127,6 @@ class KubernetesCreateApplicationController {
// a validation message will be shown. isExistingCPUReservationUnchanged and isExistingMemoryReservationUnchanged (with available resources being exceeded) is used to decide whether to show the message or not.
isExistingCPUReservationUnchanged: false,
isExistingMemoryReservationUnchanged: false,
stackNameError: '',
};
this.isAdmin = this.Authentication.isAdmin();
@@ -188,16 +186,9 @@ class KubernetesCreateApplicationController {
}
/* #endregion */
onChangeStackName(name) {
onChangeStackName(stackName) {
return this.$async(async () => {
if (KUBE_STACK_NAME_VALIDATION_REGEX.test(name) || name === '') {
this.state.stackNameError = '';
} else {
this.state.stackNameError =
"Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').";
}
this.formValues.StackName = name;
this.formValues.StackName = stackName;
});
}
@@ -653,8 +644,7 @@ class KubernetesCreateApplicationController {
const invalid = !this.isValid();
const hasNoChanges = this.isEditAndNoChangesMade();
const nonScalable = this.isNonScalable();
const stackNameInvalid = this.state.stackNameError !== '';
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || stackNameInvalid;
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
}
isUpdateApplicationViaWebEditorButtonDisabled() {
@@ -1138,9 +1128,6 @@ class KubernetesCreateApplicationController {
}
this.oldFormValues = angular.copy(this.formValues);
this.savedFormValues = angular.copy(this.formValues);
this.updateNamespaceLimits(this.namespaceWithQuota);
this.updateSliders(this.namespaceWithQuota);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {

View File

@@ -90,12 +90,7 @@
<div class="w-fit mb-4">
<stack-name-label-insight></stack-name-label-insight>
</div>
<kube-stack-name
stack-name="ctrl.formValues.StackName"
set-stack-name="(ctrl.setStackName)"
stacks="ctrl.stacks"
error="ctrl.state.stackNameError"
></kube-stack-name>
<kube-stack-name stack-name="ctrl.formValues.StackName" set-stack-name="(ctrl.setStackName)" stacks="ctrl.stacks"></kube-stack-name>
</div>
<!-- !namespace -->

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