Compare commits

..

84 Commits

Author SHA1 Message Date
prabhat khera
e075b71fef fix(api): api error message [EE-6818] 2024-03-13 12:26:09 +13:00
cmeng
6e11c10bab fix(csrf): disable csrf secure cookie EE-6787 (#11299) 2024-03-13 11:22:18 +13:00
LP B
cb9ab3b375 fix(app): views not loading when quickly navigating in app (#11279) 2024-03-12 15:16:19 +01:00
Chaim Lev-Ari
b13dac0f6d fix(docker): apply private uac to edge admin [EE-6788] (#11284) 2024-03-12 09:59:39 +02:00
cmeng
0144a98b3b fix(edge-stack): deploy button is disabled EE-6819 (#11354) 2024-03-12 17:19:45 +13:00
Prabhat Khera
64a08c59e9 address review commets (#11361) 2024-03-12 11:32:03 +13:00
Ali
1090c82beb fix(app): on create don't mention previous values [EE-6837] (#11351)
Co-authored-by: testa113 <testa113>
2024-03-11 16:43:45 +13:00
Prabhat Khera
6094dc115b fix(container): autocomplete off for create container form [EE-6761] (#11337)
* autocomplete off doe create container form

* address review commets

* remove auto complete off from forms
2024-03-11 13:38:59 +13:00
Prabhat Khera
30513695b5 fix(kube): stackname in daemonsets and statefulsets app [EE-6670] (#11353) 2024-03-11 10:04:55 +13:00
Chaim Lev-Ari
dd2be9fb1e refactor(tests): wrap tests explicitly with provider [EE-6686] (#11276) 2024-03-10 14:22:05 +02:00
Chaim Lev-Ari
e265b8b67c fix(kube/config): validate change window start [EE-6830] (#11328) 2024-03-10 09:42:29 +02:00
Matt Hook
cc1ce9412a fix(exec): improve alignment of help icon [EE-6816] (#11340) 2024-03-08 14:03:01 +13:00
Prabhat Khera
8eb8df2b30 fix(kube-stacks): change wordings [EE-6670] (#11335) 2024-03-08 12:15:27 +13:00
Ali
c0bd2dfdaf fix(matomo): stop oauth link event [EE-6779] (#11333) 2024-03-08 10:17:26 +13:00
Matt Hook
bf65a38d5a fix(exec): fix alignment and text size and alignment [EE-6816] (#11324) 2024-03-07 12:57:53 +13:00
cmeng
0ea21f2317 fix(menu): edge compute menu not clickable EE-6804 (#11320) 2024-03-07 12:11:59 +13:00
Prabhat Khera
b5f839a920 fix(stacks): make stackName kube stack specific field [EE-6670] (#11316)
* fix(stacks): make stackName kube stack specific field [EE-6670]

* fix wordings
2024-03-07 11:31:28 +13:00
Prabhat Khera
29025e7dd4 fix(UI): axios progress bar loading issue [EE-6781] (#11290) 2024-03-07 11:30:23 +13:00
Ali
692981b615 fix(time window): show errors for component [EE-6800] (#11318)
Co-authored-by: testa113 <testa113>
2024-03-07 09:03:26 +13:00
Chaim Lev-Ari
d6545b6af5 fix(kube/setup): add a11y labels [EE-6747] (#11308) 2024-03-06 14:57:03 +02:00
Matt Hook
6bbf62fe64 fix(contexthelp): remove extra slash from contexthelp docs link [EE-6780] (#11312) 2024-03-06 16:38:19 +13:00
Matt Hook
6b3ddf11d4 fix(helm): remove helm insights from the stack datatable [EE-6803] (#11313) 2024-03-06 16:36:48 +13:00
Dakota Walsh
77c9124e8a fix(datatable): title size EE-6774 (#11273) 2024-03-06 08:01:45 +13:00
Chaim Lev-Ari
2c3dcdd14e fix(docker/images): export image [EE-6807] (#11305) 2024-03-05 19:30:45 +02:00
matias-portainer
ec913b45d6 fix(edge/templates): get correct default value for selectType env vars EE-6796 (#11293) 2024-03-04 10:35:19 -03:00
Matt Hook
51c672af21 fix(kube): update doc links to match new menu structure [EE-6759] (#11266) 2024-03-01 15:37:32 +13:00
Matt Hook
ff178641be fix(help): add versioned doc links to support LTS/STS docs [EE-6780] (#11282) 2024-03-01 15:36:19 +13:00
cmeng
a43454076b fix(edge-stacks): take not-found stack as removed EE-6758 (#11249) 2024-03-01 11:50:27 +13:00
cmeng
a7eaa0f3fa fix(container): get old container info correctly EE-6716 (#11215) 2024-03-01 09:14:26 +13:00
cmeng
8ad11fc88f fix(stack): more space for add button EE-6773 (#11258) 2024-03-01 09:11:46 +13:00
Chaim Lev-Ari
43a95874f4 fix(auth): prevent unauthorized redirect on page load [EE-6777] (#11265) 2024-02-29 09:41:29 +02:00
Chaim Lev-Ari
b4f4c3212a feat(kube): add a11y props for smoke tests [EE-6747] (#11262) 2024-02-29 09:26:10 +02:00
Chaim Lev-Ari
d44f57ed6f fix(ci): prevent tests from running twice [EE-6728] (#11196) 2024-02-29 08:11:46 +02:00
Chaim Lev-Ari
eba08cdca0 fix(docker): hide write buttons for non authorized [EE-6775] (#11261) 2024-02-27 12:36:47 +02:00
Prabhat Khera
de3a3f88a0 fix(ui): autocomplete on edge custom template and stacks [EE-6761] (#11269) 2024-02-27 20:15:56 +13:00
Matt Hook
f6b2c879bc fix(kube): make app autorefresh and show system settings stay [EE-6771] (#11256) 2024-02-27 11:18:28 +13:00
Prabhat Khera
f5fbcd4d9d fix(stack): auto complete dropdown in docker stacks [EE-6761] (#11254) 2024-02-26 11:43:18 +13:00
Ali
f8b68a809f fix(app): parse nan in validation check [EE-6714] (#11247) 2024-02-26 09:20:59 +13:00
Oscar Zhou
6258c02353 fix(edge/template): validate app template env vars [EE-6743] (#11234) 2024-02-26 09:00:03 +13:00
Chaim Lev-Ari
0fd20277c1 fix(docker): prevent non admins from passing security settings [EE-6765] (#11239) 2024-02-25 11:57:19 +02:00
cmeng
988064a542 fix(stack): make web editor readonly for git template EE-6706 (#11183) 2024-02-23 13:28:20 +13:00
Matt Hook
380b23a9f5 fix(dependancies): update compose and runc [EE-6744] (#11243) 2024-02-23 11:48:49 +13:00
Prabhat Khera
158b43194c fix(ui): turn autocomplete off for git deployment [EE-6761] (#11241) 2024-02-23 08:44:00 +13:00
Ali
1bbe98379a fix(app): NaN validation for autoscaling [EE-6714] (#11238) 2024-02-22 17:36:41 +13:00
Matt Hook
8f9b265f5a fix(helm) tighten up helm requests [EE-6722] (#11233) 2024-02-22 11:35:01 +13:00
Ali
1cdd3fdfe2 fix(input): allow clearing number inputs [EE-6714] (#11187) 2024-02-21 10:43:28 +13:00
Ali
4e95139909 fix(inputlist): update warning style [EE-6737] (#11222) 2024-02-21 08:29:14 +13:00
Matt Hook
704d75596d fix(libhttp): capitalize http error responses for better display [EE-6698] (#11109) 2024-02-21 07:51:29 +13:00
Chaim Lev-Ari
a8938779bf fix(ui): check for authorization [EE-6733] (#11207) 2024-02-20 11:06:05 +02:00
Chaim Lev-Ari
bb6f4e026a fix(kube/apps): move namespace selector in apps view [EE-6612] (#11069) 2024-02-20 10:14:11 +02:00
Ali
b64166ff25 fix(app): remove insight from helm [EE-6693] (#11214)
Co-authored-by: testa113 <testa113>
2024-02-20 17:25:22 +13:00
Ali
bac1c28fa9 fix(app): set values in react autoscaling form section [EE-6740] (#11220) 2024-02-20 09:35:32 +13:00
Prabhat Khera
a17da6d2cd fix(git): update stack name for git stacks [EE-6670] (#11218) 2024-02-20 09:23:50 +13:00
Chaim Lev-Ari
24c2baf6cc feat(a11y): add labels and roles [EE-6717] (#11209) 2024-02-19 16:37:21 +02:00
Oscar Zhou
22b4d029fd fix(edge/template): custom template git fields not pre-filled [EE-6695] (#11113) 2024-02-19 08:39:16 +13:00
Ali
b126472ec7 fix(app): update app type when changing data access policy [EE-6719] (#11210)
Co-authored-by: testa113 <testa113>
2024-02-19 08:08:17 +13:00
Ali
a46fa3b2c4 fix(app): avoid duplicate env requests [EE-6727] (#11193)
Co-authored-by: testa113 <testa113>
2024-02-16 14:02:02 +13:00
Prabhat Khera
a374157d6f fix(ui): update search placeholder [EE-6667] (#11191)
* update search placeholder

* remove box selector description
2024-02-16 12:34:10 +13:00
Matt Hook
861ed662e2 fix(namespace): fix default namespace quota [EE-6700] (#11184) 2024-02-16 08:17:10 +13:00
Chaim Lev-Ari
99b89a8ec5 chore(eslint): add rule to check imports [EE-6730] (#11200) 2024-02-15 17:45:54 +02:00
Chaim Lev-Ari
95750c2339 fix(auth): export hasAuthorizations [EE-6595] (#11198) 2024-02-15 14:05:45 +02:00
Chaim Lev-Ari
165d6165dc feat(ui): restrict views by role [EE-6595] (#11071) 2024-02-15 13:29:55 +02:00
Chaim Lev-Ari
fe6ed55cab feat(edge/stacks): add app templates to deploy types [EE-6632] (#11070) 2024-02-15 09:00:57 +02:00
Chaim Lev-Ari
edea9e3481 feat(auth): add useIsEdgeAdmin hook [EE-6627] (#11101) 2024-02-14 19:50:26 -03:00
Ali
c08b5af85a fix(insight): split insight from input [EE-6693] (#11177)
Co-authored-by: testa113 <testa113>
2024-02-15 10:46:02 +13:00
Prabhat Khera
ed861044a7 Revert "fix(logs): add NOCOLOR option for use when exporting to greylog etc […" (#11178)
This reverts commit aca6d33548.
2024-02-15 06:26:22 +13:00
Chaim Lev-Ari
a83321ebe6 feat(ui): write tests [EE-6685] (#11082) 2024-02-14 17:25:32 +02:00
Ali
513cd9c9b3 fix(configs): correct 'external' display in tables [EE-6649] (#11111)
Co-authored-by: testa113 <testa113>
2024-02-14 11:48:05 +13:00
Ali
dc94bf141e fix(stacks): add app form stacks input [EE-6693] (#11105) 2024-02-14 09:01:02 +13:00
Dakota Walsh
24471a9ae1 fix(restore): add S3 teaser [EE-6675] (#11096) 2024-02-14 08:40:34 +13:00
Matt Hook
aca6d33548 fix(logs): add NOCOLOR option for use when exporting to greylog etc [EE-6696] (#11107) 2024-02-14 07:54:47 +13:00
Ali
ca77b85c65 fix(kube-owner): owner labels from resources created via manifest [EE-6647] (#11103)
Co-authored-by: testa113 <testa113>
2024-02-12 15:30:59 +13:00
Prabhat Khera
1fd4291630 fix(ui): stackname auto fill on create from manifest screen [EE-6688] (#11100)
* fix(ui): stackname auto fill on create from manifest screen [EE-6688]

* address review comment
2024-02-12 10:54:24 +13:00
Ali
08dd7f6d2a fix(auth): isAdmin redirect for wizard [EE-6669] (#11075)
Co-authored-by: testa113 <testa113>
2024-02-12 08:04:44 +13:00
Prabhat Khera
ce4b0e759c fix(ui): scroll issue [EE-6667 (#11085)
* Fix scroll issue

* fix minorissue

* address review comments

* add comment
2024-02-09 15:35:38 +13:00
Steven Kang
538e7a823b fix: pre-release build only after merging (#11098) 2024-02-09 15:26:39 +13:00
Matt Hook
956e8d3c59 fix(docs): fix swagger docs for webhook params [EE-6668] (#11089) 2024-02-09 14:44:29 +13:00
Prabhat Khera
1c5458f0d4 fix(kube): ingress path duplication issue [EE-6649] (#11087) 2024-02-09 07:49:57 +13:00
Prabhat Khera
f6085ffad7 fix stack name update issue (#11065) 2024-02-08 13:51:06 +13:00
Matt Hook
490bda2eaf fix(kube-apps): add helm insights, remove namespace insights panel [EE-6671] (#11078) 2024-02-08 11:18:48 +13:00
Prabhat Khera
d601d8eb7b fix(UI): some minor fixes [EE-6667] (#11062)
* minor tweeks for kubernetes settings

* address review comments
2024-02-06 12:17:35 +13:00
Steven Kang
b0564b9238 Pre-release as part of the CI (#11067)
* feat: add pre-release
* feat: add extension
* feat: fix typo
2024-02-05 18:29:12 +13:00
Prabhat Khera
8922585a70 keep labels on edit ingress, configmaps and secrets (#11063) 2024-02-05 16:30:31 +13:00
Ali
d7cf2284dc fix(r2a): don't set errors to undefined [EE-6665] (#11060)
Co-authored-by: testa113 <testa113>
2024-02-05 14:24:15 +13:00
1399 changed files with 12811 additions and 20340 deletions

View File

@@ -140,11 +140,9 @@ overrides:
'react/jsx-no-constructed-context-values': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
'react/jsx-props-no-spreading': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
'react/jsx-props-no-spreading': off

View File

@@ -93,10 +93,6 @@ body:
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.20.2'
- '2.20.1'
- '2.20.0'
- '2.19.5'
- '2.19.4'
- '2.19.3'
- '2.19.2'

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.9
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:

View File

@@ -18,7 +18,7 @@ on:
- ready_for_review
env:
GO_VERSION: 1.21.9
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:

View File

@@ -6,7 +6,7 @@ on:
workflow_dispatch:
env:
GO_VERSION: 1.21.9
GO_VERSION: 1.21.6
jobs:
client-dependencies:

View File

@@ -14,7 +14,7 @@ on:
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.21.9
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:

View File

@@ -1,7 +1,7 @@
name: Test
env:
GO_VERSION: 1.21.9
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
on:

View File

@@ -13,7 +13,7 @@ on:
- ready_for_review
env:
GO_VERSION: 1.21.9
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../app/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClient, QueryClientProvider } from 'react-query';
initMSW(
{

View File

@@ -26,7 +26,7 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive. Please ensure the password is correct and try again")
return errors.Wrap(err, "failed to decrypt the archive")
}
}

View File

@@ -1,7 +1,6 @@
package chisel
import (
"context"
"net"
"net/http"
"testing"
@@ -29,17 +28,12 @@ func TestPingAgentPanic(t *testing.T) {
ln, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
srv := &http.Server{Handler: mux}
errCh := make(chan error)
go func() {
errCh <- srv.Serve(ln)
require.NoError(t, http.Serve(ln, mux))
}()
s.getTunnelDetails(endpointID)
s.tunnelDetailsMap[endpointID].Port = ln.Addr().(*net.TCPAddr).Port
require.Error(t, s.pingAgent(endpointID))
require.NoError(t, srv.Shutdown(context.Background()))
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
}

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

@@ -19,7 +19,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/datastore/postinit"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/docker"
dockerclient "github.com/portainer/portainer/api/docker/client"
@@ -43,8 +42,6 @@ import (
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
@@ -460,11 +457,19 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(kubernetesClientFactory)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
reverseTunnelService.ProxyManager = proxyManager
@@ -484,19 +489,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
pendingActionsService.RegisterHandler(actions.DeletePortainerK8sRegistrySecrets, handlers.NewHandlerDeleteRegistrySecrets(authorizationService, dataStore, kubernetesClientFactory))
pendingActionsService.RegisterHandler(actions.PostInitMigrateEnvironment, handlers.NewHandlerPostInitMigrateEnvironment(authorizationService, dataStore, kubernetesClientFactory, dockerClientFactory, *flags.Assets, kubernetesDeployer))
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
proxyManager.NewProxyFactory(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
@@ -586,12 +578,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
// but some more complex migrations require access to a kubernetes or docker
// client. Therefore we run a separate migration process just before
// starting the server.
postInitMigrator := postinit.NewPostInitMigrator(
postInitMigrator := datastore.NewPostInitMigrator(
kubernetesClientFactory,
dockerClientFactory,
dataStore,
*flags.Assets,
kubernetesDeployer,
)
if err := postInitMigrator.PostInitMigrate(); err != nil {
log.Fatal().Err(err).Msg("failure during post init migrations")
@@ -660,7 +650,6 @@ func main() {
Msg("starting Portainer")
err := server.Start()
log.Info().Err(err).Msg("HTTP server exited")
}
}

View File

@@ -1,216 +1,52 @@
package crypto
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"io"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/scrypt"
)
const (
// AES GCM settings
aesGcmHeader = "AES256-GCM" // The encrypted file header
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
// NOTE: has to go with what is considered to be a simplistic in that it omits any
// authentication of the encrypted data.
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
// Argon2 settings
// Recommded settings lower memory hardware according to current OWASP recommendations
// Considering some people run portainer on a NAS I think it's prudent not to assume we're on server grade hardware
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
argon2MemoryCost = 12 * 1024
argon2TimeCost = 3
argon2Threads = 1
argon2KeyLength = 32
)
var emptySalt []byte = make([]byte, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
err := aesEncryptGCM(input, output, passphrase)
if err != nil {
return fmt.Errorf("error encrypting file: %w", err)
}
return nil
}
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
// Read file header to determine how it was encrypted
inputReader := bufio.NewReader(input)
header, err := inputReader.Peek(len(aesGcmHeader))
if err != nil {
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
}
if string(header) == aesGcmHeader {
reader, err := aesDecryptGCM(inputReader, passphrase)
if err != nil {
return nil, fmt.Errorf("error decrypting file: %w", err)
}
return reader, nil
}
// Use the previous decryption routine which has no header (to support older archives)
reader, err := aesDecryptOFB(inputReader, passphrase)
if err != nil {
return nil, fmt.Errorf("error decrypting legacy file backup: %w", err)
}
return reader, nil
}
// aesEncryptGCM reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key.
func aesEncryptGCM(input io.Reader, output io.Writer, passphrase []byte) error {
// Derive key using argon2 with a random salt
salt := make([]byte, 16) // 16 bytes salt
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return err
}
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
// Generate nonce
nonce, err := NewRandomNonce(aesgcm.NonceSize())
if err != nil {
return err
}
// write the header
if _, err := output.Write([]byte(aesGcmHeader)); err != nil {
return err
}
// Write nonce and salt to the output file
if _, err := output.Write(salt); err != nil {
return err
}
if _, err := output.Write(nonce.Value()); err != nil {
return err
}
// Buffer for reading plaintext blocks
buf := make([]byte, aesGcmBlockSize) // Adjust buffer size as needed
ciphertext := make([]byte, len(buf)+aesgcm.Overhead())
// Encrypt plaintext in blocks
for {
n, err := io.ReadFull(input, buf)
if n == 0 {
break // end of plaintext input
}
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
return err
}
// Seal encrypts the plaintext using the nonce returning the updated slice.
ciphertext = aesgcm.Seal(ciphertext[:0], nonce.Value(), buf[:n], nil)
_, err = output.Write(ciphertext)
if err != nil {
return err
}
nonce.Increment()
}
return nil
}
// aesDecryptGCM reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from.
func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
// Reader & verify header
header := make([]byte, len(aesGcmHeader))
if _, err := io.ReadFull(input, header); err != nil {
return nil, err
}
if string(header) != aesGcmHeader {
return nil, fmt.Errorf("invalid header")
}
// Read salt
salt := make([]byte, 16) // Salt size
if _, err := io.ReadFull(input, salt); err != nil {
return nil, err
}
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
// Initialize AES cipher block
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Create GCM mode with the cipher block
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Read nonce from the input reader
nonce := NewNonce(aesgcm.NonceSize())
if err := nonce.Read(input); err != nil {
return nil, err
}
// Initialize a buffer to store decrypted data
buf := bytes.Buffer{}
plaintext := make([]byte, aesGcmBlockSize)
// Decrypt the ciphertext in blocks
for {
// Read a block of ciphertext from the input reader
ciphertextBlock := make([]byte, aesGcmBlockSize+aesgcm.Overhead()) // Adjust block size as needed
n, err := io.ReadFull(input, ciphertextBlock)
if n == 0 {
break // end of ciphertext
}
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
return nil, err
}
// Decrypt the block of ciphertext
plaintext, err = aesgcm.Open(plaintext[:0], nonce.Value(), ciphertextBlock[:n], nil)
if err != nil {
return nil, err
}
_, err = buf.Write(plaintext)
if err != nil {
return nil, err
}
nonce.Increment()
}
return &buf, nil
}
// aesDecryptOFB reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.
// note: This function used to decrypt files that were encrypted without a header i.e. old archives
func aesDecryptOFB(input io.Reader, passphrase []byte) (io.Reader, error) {
var emptySalt []byte = make([]byte, 0)
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return err
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
writer := &cipher.StreamWriter{S: stream, W: output}
// Copy the input to the output, encrypting as we go.
if _, err := io.Copy(writer, input); err != nil {
return err
}
return nil
}
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
// passphrase is used to generate an encryption key.
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
@@ -223,9 +59,11 @@ func aesDecryptOFB(input io.Reader, passphrase []byte) (io.Reader, error) {
return nil, err
}
// If the key is unique for each ciphertext, then it's ok to use a zero IV.
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
reader := &cipher.StreamReader{S: stream, R: input}
return reader, nil

View File

@@ -2,7 +2,6 @@ package crypto
import (
"io"
"math/rand"
"os"
"path/filepath"
"testing"
@@ -10,19 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randBytes(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return b
}
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
const passphrase = "passphrase"
tmpdir := t.TempDir()
var (
@@ -31,99 +18,17 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1024*1024*100 + 523)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
tmpdir := t.TempDir()
var (
originFilePath = filepath.Join(tmpdir, "origin2")
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
)
content := randBytes(500)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
tmpdir := t.TempDir()
var (
originFilePath = filepath.Join(tmpdir, "origin2")
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
)
content := randBytes(500)
content := []byte("content")
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -152,7 +57,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1024 * 50)
content := []byte("content")
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
@@ -191,7 +96,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1034)
content := []byte("content")
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
@@ -212,6 +117,11 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
_, err = AesDecrypt(encryptedFileReader, []byte("garbage"))
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

View File

@@ -1,61 +0,0 @@
package crypto
import (
"crypto/rand"
"errors"
"io"
)
type Nonce struct {
val []byte
}
func NewNonce(size int) *Nonce {
return &Nonce{val: make([]byte, size)}
}
// NewRandomNonce generates a new initial nonce with the lower byte set to a random value
// This ensures there are plenty of nonce values availble before rolling over
// Based on ideas from the Secure Programming Cookbook for C and C++ by John Viega, Matt Messier
// https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch04s09.html
func NewRandomNonce(size int) (*Nonce, error) {
randomBytes := 1
if size <= randomBytes {
return nil, errors.New("nonce size must be greater than the number of random bytes")
}
randomPart := make([]byte, randomBytes)
if _, err := rand.Read(randomPart); err != nil {
return nil, err
}
zeroPart := make([]byte, size-randomBytes)
nonceVal := append(randomPart, zeroPart...)
return &Nonce{val: nonceVal}, nil
}
func (n *Nonce) Read(stream io.Reader) error {
_, err := io.ReadFull(stream, n.val)
return err
}
func (n *Nonce) Value() []byte {
return n.val
}
func (n *Nonce) Increment() error {
// Start incrementing from the least significant byte
for i := len(n.val) - 1; i >= 0; i-- {
// Increment the current byte
n.val[i]++
// Check for overflow
if n.val[i] != 0 {
// No overflow, nonce is successfully incremented
return nil
}
}
// If we reach here, it means the nonce has overflowed
return errors.New("nonce overflow")
}

View File

@@ -71,9 +71,8 @@ type (
}
PendingActionsService interface {
BaseCRUD[portainer.PendingAction, portainer.PendingActionID]
BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
GetNextIdentifier() int
DeleteByEndpointID(ID portainer.EndpointID) error
}
// EdgeStackService represents a service to manage Edge stacks

View File

@@ -1,12 +1,10 @@
package pendingactions
import (
"fmt"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
const (
@@ -14,11 +12,11 @@ const (
)
type Service struct {
dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]
dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]
}
type ServiceTx struct {
dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]
dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]
}
func NewService(connection portainer.Connection) (*Service, error) {
@@ -28,34 +26,28 @@ func NewService(connection portainer.Connection) (*Service, error) {
}
return &Service{
BaseDataService: dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]{
BaseDataService: dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]{
Bucket: BucketName,
Connection: connection,
},
}, nil
}
func (s Service) Create(config *portainer.PendingAction) error {
func (s Service) Create(config *portainer.PendingActions) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Create(config)
})
}
func (s Service) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
func (s Service) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Update(ID, config)
})
}
func (s Service) DeleteByEndpointID(ID portainer.EndpointID) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).DeleteByEndpointID(ID)
})
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
Bucket: BucketName,
Connection: service.Connection,
Tx: tx,
@@ -63,42 +55,19 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
}
}
func (s ServiceTx) Create(config *portainer.PendingAction) error {
func (s ServiceTx) Create(config *portainer.PendingActions) error {
return s.Tx.CreateObject(BucketName, func(id uint64) (int, interface{}) {
config.ID = portainer.PendingActionID(id)
config.ID = portainer.PendingActionsID(id)
config.CreatedAt = time.Now().Unix()
return int(config.ID), config
})
}
func (s ServiceTx) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
return s.BaseDataServiceTx.Update(ID, config)
}
func (s ServiceTx) DeleteByEndpointID(ID portainer.EndpointID) error {
log.Debug().Int("endpointId", int(ID)).Msg("deleting pending actions for endpoint")
pendingActions, err := s.BaseDataServiceTx.ReadAll()
if err != nil {
return fmt.Errorf("failed to retrieve pending-actions for endpoint (%d): %w", ID, err)
}
for _, pendingAction := range pendingActions {
if pendingAction.EndpointID == ID {
err := s.BaseDataServiceTx.Delete(pendingAction.ID)
if err != nil {
log.Debug().Int("endpointId", int(ID)).Msgf("failed to delete pending action: %v", err)
}
}
}
return nil
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service ServiceTx) GetNextIdentifier() int {
return service.Tx.GetNextIdentifier(BucketName)
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return service.Connection.GetNextIdentifier(BucketName)

View File

@@ -86,7 +86,6 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
EdgeStackService: store.EdgeStackService,
EdgeJobService: store.EdgeJobService,
TunnelServerService: store.TunnelServerService,
PendingActionsService: store.PendingActionsService,
}
}

View File

@@ -0,0 +1,117 @@
package datastore
import (
"context"
"github.com/docker/docker/api/types"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/rs/zerolog/log"
)
type PostInitMigrator struct {
kubeFactory *cli.ClientFactory
dockerFactory *dockerclient.ClientFactory
dataStore dataservices.DataStore
}
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *dockerclient.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
return &PostInitMigrator{
kubeFactory: kubeFactory,
dockerFactory: dockerFactory,
dataStore: dataStore,
}
}
func (migrator *PostInitMigrator) PostInitMigrate() error {
if err := migrator.PostInitMigrateIngresses(); err != nil {
return err
}
migrator.PostInitMigrateGPUs()
return nil
}
func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
endpoints, err := migrator.dataStore.Endpoint().Endpoints()
if err != nil {
return err
}
for i := range endpoints {
// Early exit if we do not need to migrate!
if !endpoints[i].PostInitMigrations.MigrateIngresses {
return nil
}
err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i])
if err != nil {
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
}
}
return nil
}
// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
// If there's an error getting the containers, we'll log it and move on
func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
environments, err := migrator.dataStore.Endpoint().Endpoints()
if err != nil {
log.Err(err).Msg("failure getting endpoints")
return
}
for i := range environments {
if environments[i].Type == portainer.DockerEnvironment {
// // Early exit if we do not need to migrate!
if !environments[i].PostInitMigrations.MigrateGPUs {
return
}
// set the MigrateGPUs flag to false so we don't run this again
environments[i].PostInitMigrations.MigrateGPUs = false
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
// create a docker client
dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil)
if err != nil {
log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name)
return
}
defer dockerClient.Close()
// get all containers
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
log.Err(err).Msg("failed to list containers")
return
}
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint
containersLoop:
for _, container := range containers {
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
if err != nil {
log.Err(err).Msg("failed to inspect container")
return
}
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
for _, deviceRequest := range deviceRequests {
if deviceRequest.Driver == "nvidia" {
environments[i].EnableGPUManagement = true
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
break containersLoop
}
}
}
}
}
}

View File

@@ -1,32 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
log.Info().Msg("cleaning up pending actions for deleted endpoints")
pendingActions, err := migrator.pendingActionsService.ReadAll()
if err != nil {
return err
}
endpoints := make(map[portainer.EndpointID]struct{})
for _, action := range pendingActions {
endpoints[action.EndpointID] = struct{}{}
}
for endpointId := range endpoints {
_, err := migrator.endpointService.Endpoint(endpointId)
if dataservices.IsErrObjectNotFound(err) {
err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,33 +0,0 @@
package migrator
import (
"github.com/segmentio/encoding/json"
"github.com/rs/zerolog/log"
)
func (migrator *Migrator) migratePendingActionsDataForDB130() error {
log.Info().Msg("Migrating pending actions data")
pendingActions, err := migrator.pendingActionsService.ReadAll()
if err != nil {
return err
}
for _, pa := range pendingActions {
actionData, err := json.Marshal(pa.ActionData)
if err != nil {
return err
}
pa.ActionData = string(actionData)
// Update the pending action
err = migrator.pendingActionsService.Update(pa.ID, &pa)
if err != nil {
return err
}
}
return nil
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
@@ -59,7 +58,6 @@ type (
edgeStackService *edgestack.Service
edgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service
pendingActionsService *pendingactions.Service
}
// MigratorParameters represents the required parameters to create a new Migrator instance.
@@ -87,7 +85,6 @@ type (
EdgeStackService *edgestack.Service
EdgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service
PendingActionsService *pendingactions.Service
}
)
@@ -117,7 +114,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
edgeStackService: parameters.EdgeStackService,
edgeJobService: parameters.EdgeJobService,
TunnelServerService: parameters.TunnelServerService,
pendingActionsService: parameters.PendingActionsService,
}
migrator.initMigrations()
@@ -236,14 +232,8 @@ func (m *Migrator) initMigrations() {
m.updateAppTemplatesVersionForDB110,
m.updateResourceOverCommitToDB110,
)
m.addMigrations("2.20.2",
m.cleanPendingActionsForDeletedEndpointsForDB111,
)
m.addMigrations("2.22.0",
m.migratePendingActionsDataForDB130,
)
// Add new migrations above...
// Add new migrations below...
// One function per migration, each versions migration funcs in the same file.
}

View File

@@ -1,98 +0,0 @@
package datastore
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
)
type cleanNAPWithOverridePolicies struct {
EndpointGroupID portainer.EndpointGroupID
}
func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
t.Run("test ConvertCleanNAPWithOverridePoliciesPayload", func(t *testing.T) {
_, store := MustNewTestStore(t, true, false)
defer store.Close()
gid := portainer.EndpointGroupID(1)
testData := []struct {
Name string
PendingAction portainer.PendingAction
Expected any
Err bool
}{
{
Name: "test actiondata with EndpointGroupID 1",
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
1,
&gid,
),
Expected: portainer.EndpointGroupID(1),
},
{
Name: "test actionData nil",
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
2,
nil,
),
Expected: nil,
},
{
Name: "test actionData empty and expected error",
PendingAction: portainer.PendingAction{
EndpointID: 2,
Action: actions.CleanNAPWithOverridePolicies,
ActionData: "",
},
Expected: nil,
Err: true,
},
}
for _, d := range testData {
err := store.PendingActions().Create(&d.PendingAction)
if err != nil {
t.Error(err)
return
}
pendingActions, err := store.PendingActions().ReadAll()
if err != nil {
t.Error(err)
return
}
for _, endpointPendingAction := range pendingActions {
t.Run(d.Name, func(t *testing.T) {
if endpointPendingAction.Action == actions.CleanNAPWithOverridePolicies {
var payload cleanNAPWithOverridePolicies
err := endpointPendingAction.UnmarshallActionData(&payload)
if d.Err && err == nil {
t.Error(err)
}
if d.Expected == nil && payload.EndpointGroupID != 0 {
t.Errorf("expected nil, got %d", payload.EndpointGroupID)
}
if d.Expected != nil {
expected := d.Expected.(portainer.EndpointGroupID)
if d.Expected != nil && expected != payload.EndpointGroupID {
t.Errorf("expected EndpointGroupID %d, got %d", expected, payload.EndpointGroupID)
}
}
}
})
}
store.PendingActions().Delete(d.PendingAction.ID)
}
})
}

View File

@@ -1,184 +0,0 @@
package postinit
import (
"context"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dockerClient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/rs/zerolog/log"
)
type PostInitMigrator struct {
kubeFactory *cli.ClientFactory
dockerFactory *dockerClient.ClientFactory
dataStore dataservices.DataStore
assetsPath string
kubernetesDeployer portainer.KubernetesDeployer
}
func NewPostInitMigrator(
kubeFactory *cli.ClientFactory,
dockerFactory *dockerClient.ClientFactory,
dataStore dataservices.DataStore,
assetsPath string,
kubernetesDeployer portainer.KubernetesDeployer,
) *PostInitMigrator {
return &PostInitMigrator{
kubeFactory: kubeFactory,
dockerFactory: dockerFactory,
dataStore: dataStore,
assetsPath: assetsPath,
kubernetesDeployer: kubernetesDeployer,
}
}
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
environments, err := postInitMigrator.dataStore.Endpoint().Endpoints()
if err != nil {
log.Error().Err(err).Msg("Error getting environments")
return err
}
for _, environment := range environments {
// edge environments will run after the server starts, in pending actions
if endpointutils.IsEdgeEndpoint(&environment) {
log.Info().Msgf("Adding pending action 'PostInitMigrateEnvironment' for environment %d", environment.ID)
err = postInitMigrator.createPostInitMigrationPendingAction(environment.ID)
if err != nil {
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environment.ID)
}
} else {
// non-edge environments will run before the server starts.
err = postInitMigrator.MigrateEnvironment(&environment)
if err != nil {
log.Error().Err(err).Msgf("Error running post-init migrations for non-edge environment %d", environment.ID)
}
}
}
return nil
}
// try to create a post init migration pending action. If it already exists, do nothing
// this function exists for readability, not reusability
// TODO: This should be moved into pending actions as part of the pending action migration
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
// If there are no pending actions for the given endpoint, create one
err := postInitMigrator.dataStore.PendingActions().Create(&portainer.PendingAction{
EndpointID: environmentID,
Action: actions.PostInitMigrateEnvironment,
})
if err != nil {
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environmentID)
}
return nil
}
// MigrateEnvironment runs migrations on a single environment
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
log.Info().Msgf("Executing post init migration for environment %d", environment.ID)
switch {
case endpointutils.IsKubernetesEndpoint(environment):
// get the kubeclient for the environment, and skip all kube migrations if there's an error
kubeclient, err := migrator.kubeFactory.GetKubeClient(environment)
if err != nil {
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err
}
// if one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
err = migrator.MigrateIngresses(*environment, kubeclient)
if err != nil {
return err
}
return nil
case endpointutils.IsDockerEndpoint(environment):
// get the docker client for the environment, and skip all docker migrations if there's an error
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
if err != nil {
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
return err
}
defer dockerClient.Close()
migrator.MigrateGPUs(*environment, dockerClient)
}
return nil
}
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateIngresses {
return nil
}
log.Debug().Msgf("Migrating ingresses for environment %d", environment.ID)
err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient)
if err != nil {
log.Error().Err(err).Msgf("Error migrating ingresses for environment %d", environment.ID)
return err
}
return nil
}
// MigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
// If there's an error getting the containers, we'll log it and move on
func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient *client.Client) error {
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
environment, err := tx.Endpoint().Endpoint(e.ID)
if err != nil {
log.Error().Err(err).Msgf("Error getting environment %d", environment.ID)
return err
}
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateGPUs {
return nil
}
log.Debug().Msgf("Migrating GPUs for environment %d", e.ID)
// get all containers
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
log.Error().Err(err).Msgf("failed to list containers for environment %d", environment.ID)
return err
}
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
containersLoop:
for _, container := range containers {
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
if err != nil {
log.Error().Err(err).Msg("failed to inspect container")
continue
}
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
for _, deviceRequest := range deviceRequests {
if deviceRequest.Driver == "nvidia" {
environment.EnableGPUManagement = true
break containersLoop
}
}
}
// set the MigrateGPUs flag to false so we don't run this again
environment.PostInitMigrations.MigrateGPUs = false
err = tx.Endpoint().UpdateEndpoint(environment.ID, environment)
if err != nil {
log.Error().Err(err).Msgf("Error updating EnableGPUManagement flag for environment %d", environment.ID)
return err
}
return nil
})
}

View File

@@ -16,9 +16,7 @@ func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService {
return tx.store.PendingActionsService.Tx(tx.tx)
}
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService { return nil }
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
return tx.store.EdgeGroupService.Tx(tx.tx)

View File

@@ -631,7 +631,6 @@
"LogoURL": "",
"OAuthSettings": {
"AccessTokenURI": "",
"AuthStyle": 0,
"AuthorizationURI": "",
"ClientID": "",
"DefaultTeamID": 0,
@@ -678,7 +677,6 @@
"Architecture": "",
"BridgeNfIp6tables": false,
"BridgeNfIptables": false,
"CDISpecDirs": null,
"CPUSet": false,
"CPUShares": false,
"CgroupDriver": "",
@@ -941,6 +939,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":2,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -13,7 +13,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/segmentio/encoding/json"
)
@@ -93,17 +93,11 @@ func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*cli
return nil, err
}
opts := []client.Opt{
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithAPIVersionNegotiation(),
client.WithHTTPClient(httpCli),
}
if nnTransport, ok := httpCli.Transport.(*NodeNameTransport); ok && nnTransport.TLSClientConfig != nil {
opts = append(opts, client.WithScheme("https"))
}
return client.NewClientWithOpts(opts...)
)
}
func createAgentClient(endpoint *portainer.Endpoint, endpointURL string, signatureService portainer.DigitalSignatureService, nodeName string, timeout *time.Duration) (*client.Client, error) {
@@ -165,7 +159,7 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error)
resp.Body = io.NopCloser(bytes.NewReader(body))
var rs []struct {
image.Summary
types.ImageSummary
Portainer struct {
Agent struct {
NodeName string

View File

@@ -119,7 +119,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
for _, network := range container.NetworkSettings.Networks {
cli.NetworkConnect(ctx, network.NetworkID, containerId, network)
}
cli.ContainerStart(ctx, containerId, dockercontainer.StartOptions{})
cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{})
})
log.Debug().Str("container", strings.Split(container.Name, "/")[1]).Msg("starting to create a new container")
@@ -135,7 +135,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
c.sr.push(func() {
log.Debug().Str("container_id", create.ID).Msg("removing the new container")
cli.ContainerStop(ctx, create.ID, dockercontainer.StopOptions{})
cli.ContainerRemove(ctx, create.ID, dockercontainer.RemoveOptions{})
cli.ContainerRemove(ctx, create.ID, types.ContainerRemoveOptions{})
})
if err != nil {
@@ -164,14 +164,14 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
// 8. start the new container
log.Debug().Str("container_id", newContainerId).Msg("starting the new container")
err = cli.ContainerStart(ctx, newContainerId, dockercontainer.StartOptions{})
err = cli.ContainerStart(ctx, newContainerId, types.ContainerStartOptions{})
if err != nil {
return nil, errors.Wrap(err, "start container error")
}
// 9. delete the old container
log.Debug().Str("container_id", containerId).Msg("starting to remove the old container")
_ = cli.ContainerRemove(ctx, containerId, dockercontainer.RemoveOptions{})
_ = cli.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{})
c.sr.disable()

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
portainer "github.com/portainer/portainer/api"
consts "github.com/portainer/portainer/api/docker/consts"
@@ -158,7 +157,7 @@ func (c *DigestClient) ServiceImageStatus(ctx context.Context, serviceID string,
return Error, nil
}
containers, err := cli.ContainerList(ctx, container.ListOptions{
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+serviceID)),
})

View File

@@ -6,7 +6,6 @@ import (
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
_container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
@@ -148,7 +147,7 @@ func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Clien
}
func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
containers, err := cli.ContainerList(context.Background(), container.ListOptions{All: true})
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
return err
}

View File

@@ -934,7 +934,7 @@ func FileExists(filePath string) (bool, error) {
func (service *Service) SafeMoveDirectory(originalPath, newPath string) error {
// 1. Backup the source directory to a different folder
backupDir := fmt.Sprintf("%s-%s", filepath.Dir(originalPath), "backup")
err := MoveDirectory(originalPath, backupDir, false)
err := MoveDirectory(originalPath, backupDir)
if err != nil {
return fmt.Errorf("failed to backup source directory: %w", err)
}
@@ -973,14 +973,14 @@ func restoreBackup(src, backupDir string) error {
return fmt.Errorf("failed to delete destination directory: %w", err)
}
err = MoveDirectory(backupDir, src, false)
err = MoveDirectory(backupDir, src)
if err != nil {
return fmt.Errorf("failed to restore backup directory: %w", err)
}
return nil
}
func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error {
func MoveDirectory(originalPath, newPath string) error {
if _, err := os.Stat(originalPath); err != nil {
return err
}
@@ -991,13 +991,7 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
}
if alreadyExists {
if !overwriteTargetPath {
return fmt.Errorf("Target path already exists")
}
if err = os.RemoveAll(newPath); err != nil {
return fmt.Errorf("failed to overwrite path %s: %s", newPath, err.Error())
}
return errors.New("Target path already exists")
}
return os.Rename(originalPath, newPath)

View File

@@ -16,7 +16,7 @@ func Test_movePath_shouldFailIfSourceDirDoesNotExist(t *testing.T) {
file1 := addFile(destinationDir, "dir", "file")
file2 := addFile(destinationDir, "file")
err := MoveDirectory(sourceDir, destinationDir, false)
err := MoveDirectory(sourceDir, destinationDir)
assert.Error(t, err, "move directory should fail when source path is missing")
assert.FileExists(t, file1, "destination dir contents should remain")
assert.FileExists(t, file2, "destination dir contents should remain")
@@ -30,7 +30,7 @@ func Test_movePath_shouldFailIfDestinationDirExists(t *testing.T) {
file3 := addFile(destinationDir, "dir", "file")
file4 := addFile(destinationDir, "file")
err := MoveDirectory(sourceDir, destinationDir, false)
err := MoveDirectory(sourceDir, destinationDir)
assert.Error(t, err, "move directory should fail when destination directory already exists")
assert.FileExists(t, file1, "source dir contents should remain")
assert.FileExists(t, file2, "source dir contents should remain")
@@ -38,22 +38,6 @@ func Test_movePath_shouldFailIfDestinationDirExists(t *testing.T) {
assert.FileExists(t, file4, "destination dir contents should remain")
}
func Test_movePath_succesIfOverwriteSetWhenDestinationDirExists(t *testing.T) {
sourceDir := t.TempDir()
file1 := addFile(sourceDir, "dir", "file")
file2 := addFile(sourceDir, "file")
destinationDir := t.TempDir()
file3 := addFile(destinationDir, "dir", "file")
file4 := addFile(destinationDir, "file")
err := MoveDirectory(sourceDir, destinationDir, true)
assert.NoError(t, err)
assert.NoFileExists(t, file1, "source dir contents should be moved")
assert.NoFileExists(t, file2, "source dir contents should be moved")
assert.FileExists(t, file3, "destination dir contents should remain")
assert.FileExists(t, file4, "destination dir contents should remain")
}
func Test_movePath_successWhenSourceExistsAndDestinationIsMissing(t *testing.T) {
tmp := t.TempDir()
sourceDir := path.Join(tmp, "source")
@@ -62,7 +46,7 @@ func Test_movePath_successWhenSourceExistsAndDestinationIsMissing(t *testing.T)
file2 := addFile(sourceDir, "file")
destinationDir := path.Join(tmp, "destination")
err := MoveDirectory(sourceDir, destinationDir, false)
err := MoveDirectory(sourceDir, destinationDir)
assert.NoError(t, err)
assert.NoFileExists(t, file1, "source dir contents should be moved")
assert.NoFileExists(t, file2, "source dir contents should be moved")

View File

@@ -38,7 +38,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
}
}
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true)
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath)
if err != nil {
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
}
@@ -48,7 +48,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
if err != nil {
cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false)
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
if restoreError != nil {
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
}

View File

@@ -75,12 +75,7 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
if settings.AuthenticationMethod == portainer.AuthenticationInternal ||
settings.AuthenticationMethod == portainer.AuthenticationOAuth ||
(settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) {
// avoid username enumeration timing attack by creating a fake user
// https://en.wikipedia.org/wiki/Timing_attack
user = &portainer.User{
Username: "portainer-fake-username",
Password: "$2a$10$abcdefghijklmnopqrstuvwx..ABCDEFGHIJKLMNOPQRSTUVWXYZ12", // fake but valid format bcrypt hash
}
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
}
}
@@ -117,11 +112,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
if errors.Is(err, httperrors.ErrUnauthorized) {
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
}
return httperror.InternalServerError("Unable to authenticate user against LDAP", err)
return httperror.Forbidden("Only initial admin is allowed to login without oauth", err)
}
if user == nil {

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

@@ -12,7 +12,6 @@ import (
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
)
type ImageResponse struct {
@@ -64,9 +63,7 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
imageUsageSet := set.Set[string]{}
if withUsage {
containers, err := cli.ContainerList(r.Context(), container.ListOptions{
All: true,
})
containers, err := cli.ContainerList(r.Context(), types.ContainerListOptions{})
if err != nil {
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
}
@@ -78,7 +75,7 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
imagesList := make([]ImageResponse, len(images))
for i, image := range images {
if len(image.RepoTags) == 0 && len(image.RepoDigests) > 0 {
if (image.RepoTags == nil || len(image.RepoTags) == 0) && (image.RepoDigests != nil && len(image.RepoDigests) > 0) {
for _, repoDigest := range image.RepoDigests {
image.RepoTags = append(image.RepoTags, repoDigest[0:strings.Index(repoDigest, "@")]+":<none>")
}

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

@@ -8,7 +8,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/tag"
"github.com/portainer/portainer/api/pendingactions/handlers"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -157,7 +156,11 @@ func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpoin
if err != nil {
// Update flag with endpoint and continue
go func(endpointID portainer.EndpointID, endpointGroupID portainer.EndpointGroupID) {
err := handler.PendingActionsService.Create(handlers.NewCleanNAPWithOverridePolicies(endpointID, &endpointGroupID))
err := handler.PendingActionsService.Create(portainer.PendingActions{
EndpointID: endpointID,
Action: "CleanNAPWithOverridePolicies",
ActionData: endpointGroupID,
})
if err != nil {
log.Error().Err(err).Msgf("Unable to create pending action to clean NAP with override policies for endpoint (%d) and endpoint group (%d).", endpointID, endpointGroupID)
}

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,26 +17,6 @@ import (
"github.com/rs/zerolog/log"
)
type DeleteMultiplePayload struct {
Endpoints []struct {
ID int `json:"id"`
Name string `json:"name"`
DeleteCluster bool `json:"deleteCluster"`
} `json:"environments"`
}
func (payload *DeleteMultiplePayload) Validate(r *http.Request) error {
if payload == nil || len(payload.Endpoints) == 0 {
return fmt.Errorf("invalid request payload; you must provide a list of nodes to delete")
}
return nil
}
type DeleteMultipleResp struct {
Name string `json:"name"`
Err error `json:"err"`
}
// @id EndpointDelete
// @summary Remove an environment(endpoint)
// @description Remove an environment(endpoint).
@@ -52,7 +31,6 @@ type DeleteMultipleResp struct {
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/{id} [delete]
// @deprecated
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
@@ -84,53 +62,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return response.Empty(w)
}
// @id EndpointDeleteMultiple
// @summary Remove multiple environment(endpoint)s
// @description Remove multiple environment(endpoint)s.
// @description **Access policy**: administrator
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param body body DeleteMultiplePayload true "List of endpoints to delete"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/remove [post]
func (handler *Handler) endpointDeleteMultiple(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var p DeleteMultiplePayload
err := request.DecodeAndValidateJSONPayload(r, &p)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
var resps []DeleteMultipleResp
for _, e := range p.Endpoints {
// Demo endpoints cannot be deleted.
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(e.ID)) {
resps = append(resps, DeleteMultipleResp{
Name: e.Name,
Err: httperrors.ErrNotAvailableInDemo,
})
continue
}
// Attempt deletion.
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return handler.deleteEndpoint(
tx,
portainer.EndpointID(e.ID),
e.DeleteCluster,
)
})
resps = append(resps, DeleteMultipleResp{Name: e.Name, Err: err})
}
return response.JSON(w, resps)
}
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) {
@@ -248,12 +179,6 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
}
// delete the 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))
if err != nil {
return httperror.InternalServerError("Unable to delete the environment from the database", err)

View File

@@ -21,8 +21,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
handler := NewHandler(testhelpers.NewTestRequestBouncer(), demo.NewService())
handler.DataStore = store
handler.ProxyManager = proxy.NewManager(nil)
handler.ProxyManager.NewProxyFactory(nil, nil, nil, nil, nil, nil, nil, nil)
handler.ProxyManager = proxy.NewManager(nil, nil, nil, nil, nil, nil, nil)
// Create all the environments and add them to the same edge group

View File

@@ -12,8 +12,8 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/docker/docker/api/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
)
@@ -39,7 +39,7 @@ func (payload *forceUpdateServicePayload) Validate(r *http.Request) error {
// @produce json
// @param id path int true "endpoint identifier"
// @param body body forceUpdateServicePayload true "details"
// @success 200 {object} swarm.ServiceUpdateResponse "Success"
// @success 200 {object} dockertypes.ServiceUpdateResponse "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "endpoint not found"
@@ -94,7 +94,7 @@ func (handler *Handler) endpointForceUpdateService(w http.ResponseWriter, r *htt
go func() {
images.EvictImageStatus(payload.ServiceID)
images.EvictImageStatus(service.Spec.Labels[consts.SwarmStackNameLabel])
containers, _ := dockerClient.ContainerList(context.TODO(), container.ListOptions{
containers, _ := dockerClient.ContainerList(context.TODO(), types.ContainerListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+payload.ServiceID)),
})

View File

@@ -9,7 +9,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/pendingactions/handlers"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -70,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 {
@@ -267,7 +265,11 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err = handler.AuthorizationService.CleanNAPWithOverridePolicies(handler.DataStore, endpoint, nil)
if err != nil {
handler.PendingActionsService.Create(handlers.NewCleanNAPWithOverridePolicies(endpoint.ID, nil))
handler.PendingActionsService.Create(portainer.PendingActions{
EndpointID: endpoint.ID,
Action: "CleanNAPWithOverridePolicies",
ActionData: nil,
})
log.Warn().Err(err).Msgf("Unable to clean NAP with override policies for endpoint (%d). Will try to update when endpoint is online.", endpoint.ID)
}
}

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)
}
@@ -627,36 +622,9 @@ func getEdgeStackStatusParam(r *http.Request) (*portainer.EdgeStackStatusType, e
portainer.EdgeStackStatusRunning,
portainer.EdgeStackStatusDeploying,
portainer.EdgeStackStatusRemoving,
portainer.EdgeStackStatusCompleted,
}, edgeStackStatus) {
return nil, errors.New("invalid edgeStackStatus parameter")
}
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/remove",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteMultiple))).Methods(http.MethodPost)
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.22.0
// @version 2.20.0
// @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

@@ -61,7 +61,8 @@ func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *htt
return httperror.InternalServerError("Unable to install a chart", err)
}
return response.JSONWithStatus(w, release, http.StatusCreated)
w.WriteHeader(http.StatusCreated)
return response.JSON(w, release)
}
func (p *installChartPayload) Validate(_ *http.Request) error {

View File

@@ -155,7 +155,7 @@ func pullImage(ctx context.Context, docker *client.Client, imageName string) err
// runContainer should be used to run a short command that returns information to stdout
// TODO: add k8s support
func runContainer(ctx context.Context, docker *client.Client, imageName, containerName string, cmdLine []string) (output string, err error) {
opts := container.ListOptions{All: true}
opts := types.ContainerListOptions{All: true}
opts.Filters = filters.NewArgs()
opts.Filters.Add("name", containerName)
existingContainers, err := docker.ContainerList(ctx, opts)
@@ -170,7 +170,7 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
}
if len(existingContainers) > 0 {
err = docker.ContainerRemove(ctx, existingContainers[0].ID, container.RemoveOptions{Force: true})
err = docker.ContainerRemove(ctx, existingContainers[0].ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
log.Error().
Str("image_name", imageName).
@@ -211,7 +211,7 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
return "", err
}
err = docker.ContainerStart(ctx, created.ID, container.StartOptions{})
err = docker.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})
if err != nil {
log.Error().
Str("image_name", imageName).
@@ -243,14 +243,14 @@ func runContainer(ctx context.Context, docker *client.Client, imageName, contain
log.Debug().Int64("status", statusCode).Msg("container wait status")
out, err := docker.ContainerLogs(ctx, created.ID, container.LogsOptions{ShowStdout: true})
out, err := docker.ContainerLogs(ctx, created.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
log.Error().Err(err).Str("image_name", imageName).Str("container_name", containerName).Msg("getting container log")
return "", err
}
err = docker.ContainerRemove(ctx, created.ID, container.RemoveOptions{})
err = docker.ContainerRemove(ctx, created.ID, types.ContainerRemoveOptions{})
if err != nil {
log.Error().
Str("image_name", imageName).

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

@@ -7,7 +7,7 @@ import (
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/pendingactions"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -89,9 +89,17 @@ func (handler *Handler) deleteKubernetesSecrets(registry *portainer.Registry) er
}
if len(failedNamespaces) > 0 {
handler.PendingActionsService.Create(
handlers.NewDeleteK8sRegistrySecrets(portainer.EndpointID(endpointId), registry.ID, failedNamespaces),
)
handler.PendingActionsService.Create(portainer.PendingActions{
EndpointID: endpointId,
Action: pendingactions.DeletePortainerK8sRegistrySecrets,
// When extracting the data, this is the type we need to pull out
// i.e. pendingactions.DeletePortainerK8sRegistrySecretsData
ActionData: pendingactions.DeletePortainerK8sRegistrySecretsData{
RegistryID: registry.ID,
Namespaces: failedNamespaces,
},
})
}
}
}

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

@@ -13,7 +13,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"golang.org/x/oauth2"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
@@ -96,11 +95,6 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
}
}
if payload.OAuthSettings != nil {
if payload.OAuthSettings.AuthStyle < oauth2.AuthStyleAutoDetect || payload.OAuthSettings.AuthStyle > oauth2.AuthStyleInHeader {
return errors.New("Invalid OAuth AuthStyle")
}
}
return nil
}
@@ -231,7 +225,6 @@ func (handler *Handler) updateSettings(tx dataservices.DataStoreTx, payload sett
settings.OAuthSettings = *payload.OAuthSettings
settings.OAuthSettings.ClientSecret = clientSecret
settings.OAuthSettings.KubeSecretKey = kubeSecret
settings.OAuthSettings.AuthStyle = payload.OAuthSettings.AuthStyle
}
if payload.EnableEdgeComputeFeatures != nil {

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

@@ -21,7 +21,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)
@@ -191,7 +190,7 @@ func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoin
}
}
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
return false, err
}

View File

@@ -23,7 +23,6 @@ type stackListOperationFilters struct {
// @description List all stacks based on the current user authorizations.
// @description Will return all stacks if using an administrator account otherwise it
// @description will only return the list of stacks the user have access to.
// @description Limited stacks will not be returned by this endpoint.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
@@ -92,55 +91,25 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
return response.JSON(w, stacks)
}
// filterStacks refines a collection of Stack instances using specified criteria.
// This function examines the provided filters: EndpointID, SwarmID, and IncludeOrphanedStacks.
// - If both EndpointID is zero and SwarmID is an empty string, the function directly returns the original stack list without any modifications.
// - If either filter is specified, it proceeds to selectively include stacks that match the criteria.
// Key Points on Business Logic:
// 1. Determining Inclusion of Orphaned Stacks:
// - The decision to include orphaned stacks is influenced by the user's role and usually set by the client (UI).
// - Administrators or environment administrators can include orphaned stacks by setting IncludeOrphanedStacks to true, reflecting their broader access rights.
// - For non-administrative users, this is typically set to false, limiting their visibility to only stacks within their purview.
// 2. Inclusion Criteria for Orphaned Stacks:
// - When IncludeOrphanedStacks is true and an EndpointID is specified (not zero), the function selects:
// a) Stacks linked to the specified EndpointID.
// b) Orphaned stacks that don't have a naming conflict with any stack associated with the EndpointID.
// - This approach is designed to avoid name conflicts within Docker Compose, which restricts the creation of multiple stacks with the same name.
// 3. Type Matching for Orphaned Stacks:
// - The function ensures that orphaned stacks are compatible with the environment's stack type (compose or swarm).
// - It filters out orphaned swarm stacks in Docker standalone environments
// - It filters out orphaned standalone stack in Docker swarm environments
// - This ensures that re-association respects the constraints of the environment and stack type.
// The outcome is a new list of stacks that align with these filtering and business logic criteria.
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters, endpoints []portainer.Endpoint) []portainer.Stack {
if filters.EndpointID == 0 && filters.SwarmID == "" {
return stacks
}
filteredStacks := make([]portainer.Stack, 0, len(stacks))
uniqueStackNames := make(map[string]struct{})
for _, stack := range stacks {
if stack.Type == portainer.DockerComposeStack && stack.EndpointID == portainer.EndpointID(filters.EndpointID) {
filteredStacks = append(filteredStacks, stack)
uniqueStackNames[stack.Name] = struct{}{}
}
if stack.Type == portainer.DockerSwarmStack && stack.SwarmID == filters.SwarmID {
filteredStacks = append(filteredStacks, stack)
uniqueStackNames[stack.Name] = struct{}{}
}
}
for _, stack := range stacks {
if filters.IncludeOrphanedStacks && isOrphanedStack(stack, endpoints) {
if (stack.Type == portainer.DockerComposeStack && filters.SwarmID == "") || (stack.Type == portainer.DockerSwarmStack && filters.SwarmID != "") {
if _, exists := uniqueStackNames[stack.Name]; !exists {
filteredStacks = append(filteredStacks, stack)
}
filteredStacks = append(filteredStacks, stack)
}
continue
}
if stack.Type == portainer.DockerComposeStack && stack.EndpointID == portainer.EndpointID(filters.EndpointID) {
filteredStacks = append(filteredStacks, stack)
}
if stack.Type == portainer.DockerSwarmStack && stack.SwarmID == filters.SwarmID {
filteredStacks = append(filteredStacks, stack)
}
}

View File

@@ -1,74 +0,0 @@
package stacks
import (
"sort"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestFilterStacks(t *testing.T) {
t.Run("filter stacks against particular endpoint and all orphaned stacks", func(t *testing.T) {
stacks := []portainer.Stack{
{ID: 1, EndpointID: 3, Name: "normal_stack", Type: portainer.DockerComposeStack},
{ID: 2, EndpointID: 4, Name: "orphaned_stack", Type: portainer.DockerComposeStack},
{ID: 3, EndpointID: 5, Name: "other_stack", Type: portainer.DockerComposeStack},
}
filters := &stackListOperationFilters{EndpointID: 3, IncludeOrphanedStacks: true}
endpoints := []portainer.Endpoint{{ID: 3}, {ID: 5}}
expectStacks := []portainer.Stack{{ID: 1}, {ID: 2}}
actualStacks := filterStacks(stacks, filters, endpoints)
isEqualStacks(t, expectStacks, actualStacks)
})
t.Run("filter unique stacks against particular endpoint and all orphaned stacks and an orphaned stack has the same name with normal stack", func(t *testing.T) {
stacks := []portainer.Stack{
{ID: 1, EndpointID: 3, Name: "normal_stack", Type: portainer.DockerComposeStack},
{ID: 2, EndpointID: 4, Name: "orphaned_stack", Type: portainer.DockerComposeStack},
{ID: 3, EndpointID: 5, Name: "other_stack", Type: portainer.DockerComposeStack},
{ID: 4, EndpointID: 4, Name: "normal_stack", Type: portainer.DockerComposeStack},
}
filters := &stackListOperationFilters{EndpointID: 3, IncludeOrphanedStacks: true}
endpoints := []portainer.Endpoint{{ID: 3}, {ID: 5}}
expectStacks := []portainer.Stack{{ID: 1}, {ID: 2}}
actualStacks := filterStacks(stacks, filters, endpoints)
isEqualStacks(t, expectStacks, actualStacks)
})
t.Run("only filter stacks against particular endpoint and no orphaned stacks", func(t *testing.T) {
stacks := []portainer.Stack{
{ID: 1, EndpointID: 3, Name: "normal_stack", Type: portainer.DockerComposeStack},
{ID: 2, EndpointID: 4, Name: "orphaned_stack", Type: portainer.DockerComposeStack},
{ID: 3, EndpointID: 5, Name: "other_stack", Type: portainer.DockerComposeStack},
{ID: 4, EndpointID: 4, Name: "normal_stack", Type: portainer.DockerComposeStack},
}
filters := &stackListOperationFilters{EndpointID: 3, IncludeOrphanedStacks: false}
endpoints := []portainer.Endpoint{{ID: 3}, {ID: 5}}
expectStacks := []portainer.Stack{{ID: 1}}
actualStacks := filterStacks(stacks, filters, endpoints)
isEqualStacks(t, expectStacks, actualStacks)
})
}
func isEqualStacks(t *testing.T, expectStacks, actualStacks []portainer.Stack) {
expectStackIDs := make([]int, len(expectStacks))
for i, stack := range expectStacks {
expectStackIDs[i] = int(stack.ID)
}
sort.Ints(expectStackIDs)
actualStackIDs := make([]int, len(actualStacks))
for i, stack := range actualStacks {
actualStackIDs[i] = int(stack.ID)
}
sort.Ints(actualStackIDs)
assert.Equal(t, expectStackIDs, actualStackIDs)
}

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

@@ -2,7 +2,6 @@ package users
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@@ -21,6 +20,9 @@ type userAccessTokenCreatePayload struct {
}
func (payload *userAccessTokenCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Password) {
return errors.New("invalid password: cannot be empty")
}
if govalidator.IsNull(payload.Description) {
return errors.New("invalid description: cannot be empty")
}
@@ -42,7 +44,6 @@ type accessTokenResponse struct {
// @summary Generate an API key for a user
// @description Generates an API key for a user.
// @description Only the calling user can generate a token for themselves.
// @description Password is required only for internal authentication.
// @description **Access policy**: restricted
// @tags users
// @security jwt
@@ -50,7 +51,7 @@ type accessTokenResponse struct {
// @produce json
// @param id path int true "User identifier"
// @param body body userAccessTokenCreatePayload true "details"
// @success 200 {object} accessTokenResponse "Created"
// @success 201 {object} accessTokenResponse "Created"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
@@ -59,13 +60,8 @@ type accessTokenResponse struct {
// @router /users/{id}/tokens [post]
func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// specifically require Cookie auth for this endpoint since API-Key based auth is not supported
jwt, _ := handler.bouncer.CookieAuthLookup(r)
if jwt == nil {
jwt, _ = handler.bouncer.JWTAuthLookup(r)
}
if jwt == nil {
return httperror.Unauthorized("Auth not supported", errors.New("Authentication required"))
if jwt, _ := handler.bouncer.CookieAuthLookup(r); jwt == nil {
return httperror.Unauthorized("Auth not supported", errors.New("Cookie Authentication required"))
}
var payload userAccessTokenCreatePayload
@@ -93,21 +89,9 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
return httperror.InternalServerError("Unable to find a user with the specified identifier inside the database", err)
}
internalAuth, err := handler.usesInternalAuthentication(portainer.UserID(userID))
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
if err != nil {
return httperror.InternalServerError("Unable to determine the authentication method", err)
}
if internalAuth {
// Internal auth requires the password field and must not be empty
if govalidator.IsNull(payload.Password) {
return httperror.BadRequest("Invalid request payload", errors.New("invalid password: cannot be empty"))
}
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
if err != nil {
return httperror.Forbidden("Current password doesn't match", errors.New("Current password does not match the password provided. Please try again"))
}
return httperror.Forbidden("Current password doesn't match", errors.New("Current password does not match the password provided. Please try again"))
}
rawAPIKey, apiKey, err := handler.apiKeyService.GenerateApiKey(*user, payload.Description)
@@ -115,20 +99,6 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
return httperror.InternalServerError("Internal Server Error", err)
}
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 {
return true, nil
}
// otherwise determine the auth method from the settings
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return false, fmt.Errorf("unable to retrieve the settings from the database: %w", err)
}
return settings.AuthenticationMethod == portainer.AuthenticationInternal, nil
w.WriteHeader(http.StatusCreated)
return response.JSON(w, accessTokenResponse{rawAPIKey, *apiKey})
}

View File

@@ -107,7 +107,7 @@ func Test_userCreateAccessToken(t *testing.T) {
body, err := io.ReadAll(rr.Body)
is.NoError(err, "ReadAll should not return error")
is.Equal(`{"message":"Auth not supported","details":"Authentication required"}`, string(body))
is.Equal(`{"message":"Auth not supported","details":"Cookie Authentication required"}`, string(body))
})
}

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

@@ -65,7 +65,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
DockerClientFactory: factory.dockerClientFactory,
}
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport, factory.gitService, factory.snapshotService)
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport, factory.gitService)
if err != nil {
return nil, err
}

View File

@@ -36,7 +36,6 @@ type (
reverseTunnelService portainer.ReverseTunnelService
dockerClientFactory *dockerclient.ClientFactory
gitService portainer.GitService
snapshotService portainer.SnapshotService
}
// TransportParameters is used to create a new Transport
@@ -64,7 +63,7 @@ type (
)
// NewTransport returns a pointer to a new Transport instance.
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport, gitService portainer.GitService, snapshotService portainer.SnapshotService) (*Transport, error) {
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport, gitService portainer.GitService) (*Transport, error) {
transport := &Transport{
endpoint: parameters.Endpoint,
dataStore: parameters.DataStore,
@@ -73,7 +72,6 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport
dockerClientFactory: parameters.DockerClientFactory,
HTTPTransport: httpTransport,
gitService: gitService,
snapshotService: snapshotService,
}
return transport, nil

View File

@@ -8,7 +8,6 @@ import (
"path"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
@@ -49,14 +48,6 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
if responseObject["Volumes"] != nil {
volumeData := responseObject["Volumes"].([]interface{})
if transport.snapshotService != nil {
// Filling snapshot data can improve the performance of getVolumeResourceID
if err = transport.snapshotService.FillSnapshotData(transport.endpoint); err != nil {
log.Info().Err(err).
Int("endpoint id", int(transport.endpoint.ID)).
Msg("snapshot is not filled into the endpoint.")
}
}
for _, volumeObject := range volumeData {
volume := volumeObject.(map[string]interface{})

View File

@@ -22,7 +22,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
proxy := &dockerLocalProxy{}
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path), factory.gitService, factory.snapshotService)
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path), factory.gitService)
if err != nil {
return nil, err
}

View File

@@ -23,7 +23,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
proxy := &dockerLocalProxy{}
dockerTransport, err := docker.NewTransport(transportParameters, newNamedPipeTransport(path), factory.gitService, factory.snapshotService)
dockerTransport, err := docker.NewTransport(transportParameters, newNamedPipeTransport(path), factory.gitService)
if err != nil {
return nil, err
}

View File

@@ -23,12 +23,11 @@ type (
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
gitService portainer.GitService
snapshotService portainer.SnapshotService
}
)
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) *ProxyFactory {
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService) *ProxyFactory {
return &ProxyFactory{
dataStore: dataStore,
signatureService: signatureService,
@@ -37,7 +36,6 @@ func NewProxyFactory(dataStore dataservices.DataStore, signatureService portaine
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
gitService: gitService,
snapshotService: snapshotService,
}
}

View File

@@ -25,24 +25,17 @@ type (
)
// NewManager initializes a new proxy Service
func NewManager(kubernetesClientFactory *cli.ClientFactory) *Manager {
func NewManager(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService) *Manager {
return &Manager{
endpointProxies: cmap.New(),
k8sClientFactory: kubernetesClientFactory,
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService),
}
}
func (manager *Manager) NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) {
manager.proxyFactory = factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
}
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
if manager.proxyFactory == nil {
return nil, fmt.Errorf("proxy factory not init")
}
proxy, err := manager.proxyFactory.NewEndpointProxy(endpoint)
if err != nil {
return nil, err
@@ -55,9 +48,6 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
// CreateAgentProxyServer creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateAgentProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
if manager.proxyFactory == nil {
return nil, fmt.Errorf("proxy factory not init")
}
return manager.proxyFactory.NewAgentProxy(endpoint)
}
@@ -84,8 +74,5 @@ func (manager *Manager) DeleteEndpointProxy(endpointID portainer.EndpointID) {
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
if manager.proxyFactory == nil {
return nil, fmt.Errorf("proxy factory not init")
}
return manager.proxyFactory.NewGitlabProxy(url)
}

View File

@@ -1,7 +1,6 @@
package security
import (
"fmt"
"net/http"
"strings"
"time"
@@ -11,7 +10,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/rs/zerolog/log"
"github.com/pkg/errors"
)
@@ -29,7 +27,6 @@ type (
AuthorizedEdgeEndpointOperation(*http.Request, *portainer.Endpoint) error
TrustedEdgeEnvironmentAccess(dataservices.DataStoreTx, *portainer.Endpoint) error
CookieAuthLookup(*http.Request) (*portainer.TokenData, error)
JWTAuthLookup(*http.Request) (*portainer.TokenData, error)
}
// RequestBouncer represents an entity that manages API request accesses
@@ -283,7 +280,7 @@ func (bouncer *RequestBouncer) mwAuthenticateFirst(tokenLookups []tokenLookup, n
for _, lookup := range tokenLookups {
resultToken, err := lookup(r)
if err != nil {
httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", httperrors.ErrUnauthorized)
httperror.WriteError(w, http.StatusUnauthorized, "Invalid token", httperrors.ErrUnauthorized)
return
}
@@ -319,7 +316,7 @@ func (bouncer *RequestBouncer) CookieAuthLookup(r *http.Request) (*portainer.Tok
tokenData, err := bouncer.jwtService.ParseAndVerifyToken(token)
if err != nil {
return nil, err
return nil, ErrInvalidKey
}
return tokenData, nil
@@ -335,7 +332,7 @@ func (bouncer *RequestBouncer) JWTAuthLookup(r *http.Request) (*portainer.TokenD
tokenData, err := bouncer.jwtService.ParseAndVerifyToken(token)
if err != nil {
return nil, err
return nil, ErrInvalidKey
}
return tokenData, nil
@@ -369,8 +366,7 @@ func (bouncer *RequestBouncer) apiKeyLookup(r *http.Request) (*portainer.TokenDa
Role: user.Role,
}
if _, _, err := bouncer.jwtService.GenerateToken(tokenData); err != nil {
log.Debug().Err(err).Msg("Failed to generate token")
return nil, fmt.Errorf("failed to generate token")
return nil, ErrInvalidKey
}
if now := time.Now().UTC().Unix(); now-apiKey.LastUsed > 60 { // [seconds]

View File

@@ -61,6 +61,7 @@ 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,8 +382,7 @@ func (server *Server) Start() error {
go shutdown(server.ShutdownCtx, httpsServer)
// Temporarily disable for EE-6905 until we have a solution for the snapshotter
// go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
go snapshot.NewBackgroundSnapshotter(server.DataStore, server.ReverseTunnelService)
return httpsServer.ListenAndServeTLS("", "")
}

View File

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

@@ -54,10 +54,6 @@ func (testRequestBouncer) CookieAuthLookup(r *http.Request) (*portainer.TokenDat
return nil, nil
}
func (testRequestBouncer) JWTAuthLookup(r *http.Request) (*portainer.TokenData, error) {
return nil, nil
}
// AddTestSecurityCookie adds a security cookie to the request
func AddTestSecurityCookie(r *http.Request, jwt string) {
r.AddCookie(&http.Cookie{

View File

@@ -13,8 +13,6 @@ import (
"github.com/rs/zerolog/log"
)
const year = time.Hour * 24 * 365
// scope represents JWT scopes that are supported in JWT claims.
type scope string
@@ -31,7 +29,7 @@ type claims struct {
Role int `json:"role"`
Scope scope `json:"scope"`
ForceChangePassword bool `json:"forceChangePassword"`
jwt.RegisteredClaims
jwt.StandardClaims
}
var (
@@ -100,7 +98,7 @@ func (service *Service) defaultExpireAt() time.Time {
// GenerateToken generates a new JWT token.
func (service *Service) GenerateToken(data *portainer.TokenData) (string, time.Time, error) {
expiryTime := service.defaultExpireAt()
token, err := service.generateSignedToken(data, expiryTime, defaultScope)
token, err := service.generateSignedToken(data, expiryTime.Unix(), defaultScope)
return token, expiryTime, err
}
@@ -123,7 +121,7 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
if err != nil {
return nil, errInvalidJWTToken
}
if user.TokenIssueAt > cl.RegisteredClaims.IssuedAt.Unix() {
if user.TokenIssueAt > cl.StandardClaims.IssuedAt {
return nil, errInvalidJWTToken
}
@@ -158,7 +156,7 @@ func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration
service.userSessionTimeout = userSessionDuration
}
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt time.Time, scope scope) (string, error) {
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64, scope scope) (string, error) {
secret, found := service.secrets[scope]
if !found {
return "", fmt.Errorf("invalid scope: %v", scope)
@@ -172,7 +170,7 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt
if settings.IsDockerDesktopExtension {
// Set expiration to 99 years for docker desktop extension.
log.Info().Msg("detected docker desktop extension mode")
expiresAt = time.Now().Add(year * 99)
expiresAt = time.Now().Add(time.Hour * 8760 * 99).Unix()
}
cl := claims{
@@ -181,17 +179,12 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt
Role: int(data.Role),
Scope: scope,
ForceChangePassword: data.ForceChangePassword,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(expiresAt),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expiresAt,
IssuedAt: time.Now().Unix(),
},
}
// If expiresAt is set to a zero value, the token should never expire
if expiresAt.IsZero() {
cl.RegisteredClaims.ExpiresAt = nil
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
signedToken, err := token.SignedString(secret)
if err != nil {

View File

@@ -18,10 +18,9 @@ func (service *Service) GenerateTokenForKubeconfig(data *portainer.TokenData) (s
return "", err
}
// https://go.dev/play/p/bOrt6cQpA0I time.Time defaults to a zero value which is 0001-01-01 00:00:00 +0000 UTC
var expiryAt time.Time
if expiryDuration > time.Duration(0) {
expiryAt = time.Now().Add(expiryDuration)
expiryAt := time.Now().Add(expiryDuration).Unix()
if expiryDuration == time.Duration(0) {
expiryAt = 0
}
return service.generateSignedToken(data, expiryAt, kubeConfigScope)

View File

@@ -43,14 +43,14 @@ func TestService_GenerateTokenForKubeconfig(t *testing.T) {
name string
fields fields
args args
wantExpiresAt *jwt.NumericDate
wantExpiresAt int64
wantErr bool
}{
{
name: "kubeconfig no expiry",
fields: myFields,
args: myArgs,
wantExpiresAt: nil,
wantExpiresAt: 0,
wantErr: false,
},
}

View File

@@ -20,7 +20,7 @@ func TestGenerateSignedToken(t *testing.T) {
ID: 1,
Role: 1,
}
expiresAt := time.Now().Add(1 * time.Hour)
expiresAt := time.Now().Add(1 * time.Hour).Unix()
generatedToken, err := svc.generateSignedToken(token, expiresAt, defaultScope)
assert.NoError(t, err, "failed to generate a signed token")
@@ -36,7 +36,7 @@ func TestGenerateSignedToken(t *testing.T) {
assert.Equal(t, token.Username, tokenClaims.Username)
assert.Equal(t, int(token.ID), tokenClaims.UserID)
assert.Equal(t, int(token.Role), tokenClaims.Role)
assert.Equal(t, jwt.NewNumericDate(expiresAt), tokenClaims.ExpiresAt)
assert.Equal(t, expiresAt, tokenClaims.ExpiresAt)
}
func TestGenerateSignedToken_InvalidScope(t *testing.T) {
@@ -49,7 +49,7 @@ func TestGenerateSignedToken_InvalidScope(t *testing.T) {
ID: 1,
Role: 1,
}
expiresAt := time.Now().Add(1 * time.Hour)
expiresAt := time.Now().Add(1 * time.Hour).Unix()
_, err = svc.generateSignedToken(token, expiresAt, "testing")
assert.Error(t, err)

View File

@@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
@@ -81,30 +80,21 @@ func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID)
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
// If no client is registered, it will create a new client, register it, and returns it.
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
factory.mu.Lock()
key := strconv.Itoa(int(endpoint.ID))
if client, ok := factory.endpointClients[key]; ok {
factory.mu.Unlock()
return client, nil
}
factory.mu.Unlock()
// EE-6901: Do not lock
client, err := factory.createCachedAdminKubeClient(endpoint)
if err != nil {
return nil, err
}
factory.mu.Lock()
defer factory.mu.Unlock()
// The lock was released before the client was created,
// so we need to check again
if c, ok := factory.endpointClients[key]; ok {
return c, nil
}
key := strconv.Itoa(int(endpoint.ID))
client, ok := factory.endpointClients[key]
if !ok {
var err error
factory.endpointClients[key] = client
client, err = factory.createCachedAdminKubeClient(endpoint)
if err != nil {
return nil, err
}
factory.endpointClients[key] = client
}
return client, nil
}
@@ -252,10 +242,6 @@ func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*re
}
signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
config.Insecure = true
config.QPS = DefaultKubeClientQPS
config.Burst = DefaultKubeClientBurst
@@ -291,111 +277,106 @@ func buildLocalConfig() (*rest.Config, error) {
return config, nil
}
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint, datastore dataservices.DataStore, cli *KubeClient) error {
return datastore.UpdateTx(func(tx dataservices.DataStoreTx) error {
environment, err := tx.Endpoint().Endpoint(e.ID)
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error {
// classes is a list of controllers which have been manually added to the
// cluster setup view. These need to all be allowed globally, but then
// blocked in specific namespaces which they were not previously allowed in.
classes := e.Kubernetes.Configuration.IngressClasses
// We need a kube client to gather namespace level permissions. In pre-2.16
// versions of portainer, the namespace level permissions were stored by
// creating an actual ingress rule in the cluster with a particular
// annotation indicating that it's name (the class name) should be allowed.
cli, err := factory.GetKubeClient(e)
if err != nil {
return err
}
detected, err := cli.GetIngressControllers()
if err != nil {
return err
}
// newControllers is a set of all currently detected controllers.
newControllers := make(map[string]struct{})
for _, controller := range detected {
newControllers[controller.ClassName] = struct{}{}
}
namespaces, err := cli.GetNamespaces()
if err != nil {
return err
}
// Set of namespaces, if any, in which "allow none" should be true.
allow := make(map[string]map[string]struct{})
for _, c := range classes {
allow[c.Name] = make(map[string]struct{})
}
allow["none"] = make(map[string]struct{})
for namespace := range namespaces {
// Compare old annotations with currently detected controllers.
ingresses, err := cli.GetIngresses(namespace)
if err != nil {
log.Error().Err(err).Msgf("Error retrieving environment %d", e.ID)
return err
return fmt.Errorf("failure getting ingresses during migration")
}
for _, ingress := range ingresses {
oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"]
if !ok {
// Skip rules without our old annotation.
continue
}
// classes is a list of controllers which have been manually added to the
// cluster setup view. These need to all be allowed globally, but then
// blocked in specific namespaces which they were not previously allowed in.
classes := environment.Kubernetes.Configuration.IngressClasses
if _, ok := newControllers[oldController]; ok {
// Skip rules which match a detected controller.
// TODO: Allow this particular controller.
allow[oldController][ingress.Namespace] = struct{}{}
continue
}
// In pre-2.16 versions of portainer, the namespace level permissions were stored by
// creating an actual ingress rule in the cluster with a particular
// annotation indicating that it's name (the class name) should be allowed.
detected, err := cli.GetIngressControllers()
if err != nil {
log.Error().Err(err).Msgf("Error getting ingress controllers in environment %d", environment.ID)
return err
allow["none"][ingress.Namespace] = struct{}{}
}
}
// newControllers is a set of all currently detected controllers.
newControllers := make(map[string]struct{})
for _, controller := range detected {
newControllers[controller.ClassName] = struct{}{}
}
namespaces, err := cli.GetNamespaces()
if err != nil {
log.Error().Err(err).Msgf("Error getting namespaces in environment %d", environment.ID)
return err
}
// Set of namespaces, if any, in which "allow none" should be true.
allow := make(map[string]map[string]struct{})
for _, c := range classes {
allow[c.Name] = make(map[string]struct{})
}
allow["none"] = make(map[string]struct{})
// Locally, disable "allow none" for namespaces not inside shouldAllowNone.
var newClasses []portainer.KubernetesIngressClassConfig
for _, c := range classes {
var blocked []string
for namespace := range namespaces {
// Compare old annotations with currently detected controllers.
ingresses, err := cli.GetIngresses(namespace)
if err != nil {
log.Error().Err(err).Msgf("Error getting ingresses in environment %d", environment.ID)
return err
}
for _, ingress := range ingresses {
oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"]
if !ok {
// Skip rules without our old annotation.
continue
}
if _, ok := newControllers[oldController]; ok {
// Skip rules which match a detected controller.
// TODO: Allow this particular controller.
allow[oldController][ingress.Namespace] = struct{}{}
continue
}
allow["none"][ingress.Namespace] = struct{}{}
if _, ok := allow[c.Name][namespace]; ok {
continue
}
blocked = append(blocked, namespace)
}
// Locally, disable "allow none" for namespaces not inside shouldAllowNone.
var newClasses []portainer.KubernetesIngressClassConfig
for _, c := range classes {
var blocked []string
for namespace := range namespaces {
if _, ok := allow[c.Name][namespace]; ok {
continue
}
blocked = append(blocked, namespace)
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
Name: c.Name,
Type: c.Type,
GloballyBlocked: false,
BlockedNamespaces: blocked,
})
}
// Handle "none".
if len(allow["none"]) != 0 {
e.Kubernetes.Configuration.AllowNoneIngressClass = true
var disallowNone []string
for namespace := range namespaces {
if _, ok := allow["none"][namespace]; ok {
continue
}
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
Name: c.Name,
Type: c.Type,
GloballyBlocked: false,
BlockedNamespaces: blocked,
})
disallowNone = append(disallowNone, namespace)
}
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
Name: "none",
Type: "custom",
GloballyBlocked: false,
BlockedNamespaces: disallowNone,
})
}
// Handle "none".
if len(allow["none"]) != 0 {
environment.Kubernetes.Configuration.AllowNoneIngressClass = true
var disallowNone []string
for namespace := range namespaces {
if _, ok := allow["none"][namespace]; ok {
continue
}
disallowNone = append(disallowNone, namespace)
}
newClasses = append(newClasses, portainer.KubernetesIngressClassConfig{
Name: "none",
Type: "custom",
GloballyBlocked: false,
BlockedNamespaces: disallowNone,
})
}
environment.Kubernetes.Configuration.IngressClasses = newClasses
environment.PostInitMigrations.MigrateIngresses = false
return tx.Endpoint().UpdateEndpoint(environment.ID, environment)
})
e.Kubernetes.Configuration.IngressClasses = newClasses
e.PostInitMigrations.MigrateIngresses = false
return factory.dataStore.Endpoint().UpdateEndpoint(e.ID, e)
}

View File

@@ -125,27 +125,12 @@ func GetNamespace(manifestYaml []byte) (string, error) {
return "", errors.Wrap(err, "failed to unmarshal yaml manifest when obtaining namespace")
}
kind, ok := m["kind"].(string)
if !ok {
return "", errors.New("invalid kubernetes manifest, missing 'kind' field")
}
if _, ok := m["metadata"]; ok {
var namespace interface{}
var ok bool
if strings.EqualFold(kind, "namespace") {
namespace, ok = m["metadata"].(map[string]interface{})["name"]
} else {
namespace, ok = m["metadata"].(map[string]interface{})["namespace"]
}
if ok {
if v, ok := namespace.(string); ok {
return v, nil
}
return "", errors.New("invalid kubernetes manifest, 'namespace' field is not a string")
if namespace, ok := m["metadata"].(map[string]interface{})["namespace"]; ok {
return namespace.(string), nil
}
}
return "", nil
}

View File

@@ -648,7 +648,7 @@ func Test_GetNamespace(t *testing.T) {
input: `apiVersion: v1
kind: Namespace
metadata:
name: test-namespace
namespace: test-namespace
`,
want: "test-namespace",
},

View File

@@ -75,14 +75,7 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer.
userDN, err := searchUser(username, connection, settings.SearchSettings)
if err != nil {
if errors.Is(err, errUserNotFound) {
// prevent user enumeration timing attack by attempting the bind with a fake user
// and whatever password was provided should definately fail
// https://en.wikipedia.org/wiki/Timing_attack
userDN = "portainer-fake-ldap-username"
} else {
return err
}
return err
}
err = connection.Bind(userDN, password)

View File

@@ -172,9 +172,8 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
endpoint := oauth2.Endpoint{
AuthURL: configuration.AuthorizationURI,
TokenURL: configuration.AccessTokenURI,
AuthStyle: configuration.AuthStyle,
AuthURL: configuration.AuthorizationURI,
TokenURL: configuration.AccessTokenURI,
}
return &oauth2.Config{

View File

@@ -1,50 +0,0 @@
package portainer
import "github.com/segmentio/encoding/json"
type (
PendingActionID int
PendingAction struct {
ID PendingActionID `json:"ID"`
EndpointID EndpointID `json:"EndpointID"`
Action string `json:"Action"`
ActionData any `json:"ActionData"`
CreatedAt int64 `json:"CreatedAt"`
}
PendingActionHandler interface {
Execute(PendingAction) error
}
)
// MarshalJSON marshals the PendingAction struct to JSON
// and converts the ActionData field to an embedded json string
// This makes unmarshalling the ActionData field easier
func (pa PendingAction) MarshalJSON() ([]byte, error) {
// Create a map to hold the marshalled fields
data := map[string]any{
"ID": pa.ID,
"EndpointID": pa.EndpointID,
"Action": pa.Action,
"CreatedAt": pa.CreatedAt,
}
actionDataBytes, err := json.Marshal(pa.ActionData)
if err != nil {
return nil, err
}
data["ActionData"] = string(actionDataBytes)
// Marshal the map
return json.Marshal(data)
}
// Unmarshal the ActionData field from a string to a specific type.
func (pa PendingAction) UnmarshallActionData(v any) error {
s, ok := pa.ActionData.(string)
if !ok {
return nil
}
return json.Unmarshal([]byte(s), v)
}

View File

@@ -1,7 +0,0 @@
package actions
const (
CleanNAPWithOverridePolicies = "CleanNAPWithOverridePolicies"
DeletePortainerK8sRegistrySecrets = "DeletePortainerK8sRegistrySecrets"
PostInitMigrateEnvironment = "PostInitMigrateEnvironment"
)

View File

@@ -0,0 +1,76 @@
package pendingactions
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type DeletePortainerK8sRegistrySecretsData struct {
RegistryID portainer.RegistryID `json:"RegistryID"`
Namespaces []string `json:"Namespaces"`
}
func (service *PendingActionsService) DeleteKubernetesRegistrySecrets(endpoint *portainer.Endpoint, registryData *DeletePortainerK8sRegistrySecretsData) error {
if endpoint == nil || registryData == nil {
return nil
}
kubeClient, err := service.clientFactory.GetKubeClient(endpoint)
if err != nil {
return err
}
for _, namespace := range registryData.Namespaces {
err = kubeClient.DeleteRegistrySecret(registryData.RegistryID, namespace)
if err != nil {
return err
}
}
return nil
}
// Failure in this code is basically a bug. So if we get one we should log it and continue.
func convertToDeletePortainerK8sRegistrySecretsData(actionData interface{}) (*DeletePortainerK8sRegistrySecretsData, error) {
var registryData DeletePortainerK8sRegistrySecretsData
// Due to the way data is stored and subsequently read from the database, we can't directly type assert the actionData to
// the type DeletePortainerK8sRegistrySecretsData. It's stored as a map[string]interface{} and we need to extract the
// data from that map.
if data, ok := actionData.(map[string]interface{}); ok {
for key, value := range data {
switch key {
case "Namespaces":
if namespaces, ok := value.([]interface{}); ok {
registryData.Namespaces = make([]string, len(namespaces))
for i, ns := range namespaces {
if namespace, ok := ns.(string); ok {
registryData.Namespaces[i] = namespace
}
}
} else {
// we shouldn't ever see this. It's a bug if we do.
log.Debug().Msgf("DeletePortainerK8sRegistrySecrets: Failed to convert Namespaces to []interface{}")
}
case "RegistryID":
if registryID, ok := value.(float64); ok {
registryData.RegistryID = portainer.RegistryID(registryID)
} else {
// we shouldn't ever see this. It's a bug if we do.
log.Debug().Msgf("DeletePortainerK8sRegistrySecrets: Failed to convert RegistryID to float64")
}
}
}
log.Debug().Msgf("DeletePortainerK8sRegistrySecrets: %+v", registryData)
} else {
// this should not happen. It's a bug if it does. As the actionData is defined
// by what portainer puts in it. It never comes from a user or external source so it shouldn't fail.
// Nevertheless we should check it in case of db corruption or developer mistake down the road
return nil, fmt.Errorf("type assertion failed in convertToDeletePortainerK8sRegistrySecretsData")
}
return &registryData, nil
}

View File

@@ -1,88 +0,0 @@
package handlers
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/rs/zerolog/log"
)
type (
cleanNAPWithOverridePolicies struct {
EndpointGroupID portainer.EndpointGroupID
}
HandlerCleanNAPWithOverridePolicies struct {
authorizationService *authorization.Service
dataStore dataservices.DataStore
}
)
// NewCleanNAPWithOverridePolicies creates a new CleanNAPWithOverridePolicies pending action
func NewCleanNAPWithOverridePolicies(endpointID portainer.EndpointID, gid *portainer.EndpointGroupID) portainer.PendingAction {
pendingAction := portainer.PendingAction{
EndpointID: endpointID,
Action: actions.CleanNAPWithOverridePolicies,
}
if gid != nil {
pendingAction.ActionData = cleanNAPWithOverridePolicies{
EndpointGroupID: *gid,
}
}
return pendingAction
}
// NewHandlerCleanNAPWithOverridePolicies creates a new handler to execute CleanNAPWithOverridePolicies pending action
func NewHandlerCleanNAPWithOverridePolicies(
authorizationService *authorization.Service,
dataStore dataservices.DataStore,
) *HandlerCleanNAPWithOverridePolicies {
return &HandlerCleanNAPWithOverridePolicies{
authorizationService: authorizationService,
dataStore: dataStore,
}
}
func (h *HandlerCleanNAPWithOverridePolicies) Execute(pa portainer.PendingAction) error {
endpoint, err := h.dataStore.Endpoint().Endpoint(pa.EndpointID)
if err != nil {
log.Debug().Msgf("failed to retrieve environment %d: %v", pa.EndpointID, err)
return nil
}
if pa.ActionData == nil {
h.authorizationService.CleanNAPWithOverridePolicies(h.dataStore, endpoint, nil)
return nil
}
var payload cleanNAPWithOverridePolicies
err = pa.UnmarshallActionData(&payload)
if err != nil {
log.Error().Err(err).Msgf("Error unmarshalling endpoint group ID for cleaning NAP with override policies for environment %d", endpoint.ID)
return fmt.Errorf("failed to unmarshal endpoint group ID for cleaning NAP with override policies for environment %d: %w", endpoint.ID, err)
}
if payload.EndpointGroupID == 0 {
h.authorizationService.CleanNAPWithOverridePolicies(h.dataStore, endpoint, nil)
return nil
}
endpointGroup, err := h.dataStore.EndpointGroup().Read(portainer.EndpointGroupID(payload.EndpointGroupID))
if err != nil {
log.Error().Err(err).Msgf("Error reading environment group to clean NAP with override policies for environment %d and environment group %d", endpoint.ID, endpointGroup.ID)
return fmt.Errorf("failed to retrieve environment group %d: %w", payload.EndpointGroupID, err)
}
err = h.authorizationService.CleanNAPWithOverridePolicies(h.dataStore, endpoint, endpointGroup)
if err != nil {
log.Error().Err(err).Msgf("Error cleaning NAP with override policies for environment %d and environment group %d", endpoint.ID, endpointGroup.ID)
return fmt.Errorf("failed to clean NAP with override policies for environment %d and environment group %d: %w", endpoint.ID, endpointGroup.ID, err)
}
return nil
}

View File

@@ -1,80 +0,0 @@
package handlers
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/authorization"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/rs/zerolog/log"
)
type (
HandlerDeleteK8sRegistrySecrets struct {
authorizationService *authorization.Service
dataStore dataservices.DataStore
kubeFactory *kubecli.ClientFactory
}
deleteK8sRegistrySecretsData struct {
RegistryID portainer.RegistryID `json:"RegistryID"`
Namespaces []string `json:"Namespaces"`
}
)
// NewDeleteK8sRegistrySecrets creates a new DeleteK8sRegistrySecrets pending action
func NewDeleteK8sRegistrySecrets(endpointID portainer.EndpointID, registryID portainer.RegistryID, namespaces []string) portainer.PendingAction {
return portainer.PendingAction{
EndpointID: endpointID,
Action: actions.DeletePortainerK8sRegistrySecrets,
ActionData: &deleteK8sRegistrySecretsData{
RegistryID: registryID,
Namespaces: namespaces,
},
}
}
// NewHandlerDeleteRegistrySecrets creates a new handler to execute DeleteK8sRegistrySecrets pending action
func NewHandlerDeleteRegistrySecrets(
authorizationService *authorization.Service,
dataStore dataservices.DataStore,
kubeFactory *kubecli.ClientFactory,
) *HandlerDeleteK8sRegistrySecrets {
return &HandlerDeleteK8sRegistrySecrets{
authorizationService: authorizationService,
dataStore: dataStore,
kubeFactory: kubeFactory,
}
}
func (h *HandlerDeleteK8sRegistrySecrets) Execute(pa portainer.PendingAction) error {
if pa.ActionData == nil {
return nil
}
endpoint, err := h.dataStore.Endpoint().Endpoint(pa.EndpointID)
if err != nil {
log.Debug().Msgf("failed to retrieve environment %d: %v", pa.EndpointID, err)
return nil
}
var registryData deleteK8sRegistrySecretsData
err = pa.UnmarshallActionData(&registryData)
if err != nil {
return err
}
kubeClient, err := h.kubeFactory.GetKubeClient(endpoint)
if err != nil {
return err
}
for _, namespace := range registryData.Namespaces {
err = kubeClient.DeleteRegistrySecret(registryData.RegistryID, namespace)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,64 +0,0 @@
package handlers
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore/postinit"
dockerClient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/internal/authorization"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/rs/zerolog/log"
)
type HandlerPostInitMigrateEnvironment struct {
authorizationService *authorization.Service
dataStore dataservices.DataStore
kubeFactory *kubecli.ClientFactory
dockerFactory *dockerClient.ClientFactory
assetsPath string
kubernetesDeployer portainer.KubernetesDeployer
}
// NewPostInitMigrateEnvironment creates a new PostInitMigrateEnvironment pending action
func NewHandlerPostInitMigrateEnvironment(
authorizationService *authorization.Service,
dataStore dataservices.DataStore,
kubeFactory *kubecli.ClientFactory,
dockerFactory *dockerClient.ClientFactory,
assetsPath string,
kubernetesDeployer portainer.KubernetesDeployer,
) *HandlerPostInitMigrateEnvironment {
return &HandlerPostInitMigrateEnvironment{
authorizationService: authorizationService,
dataStore: dataStore,
kubeFactory: kubeFactory,
dockerFactory: dockerFactory,
assetsPath: assetsPath,
kubernetesDeployer: kubernetesDeployer,
}
}
func (h *HandlerPostInitMigrateEnvironment) Execute(pa portainer.PendingAction) error {
endpoint, err := h.dataStore.Endpoint().Endpoint(pa.EndpointID)
if err != nil {
log.Debug().Msgf("failed to retrieve environment %d: %v", pa.EndpointID, err)
return nil
}
postInitMigrator := postinit.NewPostInitMigrator(
h.kubeFactory,
h.dockerFactory,
h.dataStore,
h.assetsPath,
h.kubernetesDeployer,
)
err = postInitMigrator.MigrateEnvironment(endpoint)
if err != nil {
log.Error().Err(err).Msgf("Error running post-init migrations for edge environment %d", endpoint.ID)
return fmt.Errorf("failed running post-init migrations for edge environment %d: %w", endpoint.ID, err)
}
return nil
}

View File

@@ -1,123 +1,137 @@
package pendingactions
import (
"context"
"fmt"
"reflect"
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/authorization"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/rs/zerolog/log"
)
type PendingActionsService struct {
kubeFactory *kubecli.ClientFactory
dataStore dataservices.DataStore
mu sync.Mutex
}
const (
CleanNAPWithOverridePolicies = "CleanNAPWithOverridePolicies"
DeletePortainerK8sRegistrySecrets = "DeletePortainerK8sRegistrySecrets"
)
var handlers = make(map[string]portainer.PendingActionHandler)
type (
PendingActionsService struct {
authorizationService *authorization.Service
clientFactory *kubecli.ClientFactory
dataStore dataservices.DataStore
shutdownCtx context.Context
mu sync.Mutex
}
)
func NewService(
dataStore dataservices.DataStore,
kubeFactory *kubecli.ClientFactory,
clientFactory *kubecli.ClientFactory,
authorizationService *authorization.Service,
shutdownCtx context.Context,
) *PendingActionsService {
return &PendingActionsService{
dataStore: dataStore,
kubeFactory: kubeFactory,
mu: sync.Mutex{},
dataStore: dataStore,
shutdownCtx: shutdownCtx,
authorizationService: authorizationService,
clientFactory: clientFactory,
mu: sync.Mutex{},
}
}
func (service *PendingActionsService) RegisterHandler(name string, handler portainer.PendingActionHandler) {
handlers[name] = handler
func (service *PendingActionsService) Create(r portainer.PendingActions) error {
return service.dataStore.PendingActions().Create(&r)
}
func (service *PendingActionsService) Create(action portainer.PendingAction) error {
// Check if this pendingAction already exists
pendingActions, err := service.dataStore.PendingActions().ReadAll()
if err != nil {
return fmt.Errorf("failed to retrieve pending actions: %w", err)
}
func (service *PendingActionsService) Execute(id portainer.EndpointID) error {
for _, dba := range pendingActions {
// Same endpoint, same action and data, don't create a repeat
if dba.EndpointID == action.EndpointID && dba.Action == action.Action &&
reflect.DeepEqual(dba.ActionData, action.ActionData) {
log.Debug().Msgf("pending action %s already exists for environment %d, skipping...", action.Action, action.EndpointID)
return nil
}
}
return service.dataStore.PendingActions().Create(&action)
}
func (service *PendingActionsService) Execute(id portainer.EndpointID) {
service.mu.Lock()
defer service.mu.Unlock()
endpoint, err := service.dataStore.Endpoint().Endpoint(id)
if err != nil {
log.Debug().Msgf("failed to retrieve environment %d: %v", id, err)
return
return fmt.Errorf("failed to retrieve environment %d: %w", id, err)
}
isKubernetesEndpoint := endpointutils.IsKubernetesEndpoint(endpoint) && !endpointutils.IsEdgeEndpoint(endpoint)
// EndpointStatusUp is only relevant for non-Kubernetes endpoints
// Sometimes the endpoint is UP but the status is not updated in the database
if !isKubernetesEndpoint {
if endpoint.Status != portainer.EndpointStatusUp {
return
}
} else {
// For Kubernetes endpoints, we need to check if the client can be created
if _, err := service.kubeFactory.GetKubeClient(endpoint); err != nil {
return
}
if endpoint.Status != portainer.EndpointStatusUp {
log.Debug().Msgf("Environment %q (id: %d) is not up", endpoint.Name, id)
return fmt.Errorf("environment %q (id: %d) is not up: %w", endpoint.Name, id, err)
}
pendingActions, err := service.dataStore.PendingActions().ReadAll()
if err != nil {
log.Warn().Msgf("failed to read pending actions: %v", err)
return
log.Error().Err(err).Msgf("failed to retrieve pending actions")
return fmt.Errorf("failed to retrieve pending actions for environment %d: %w", id, err)
}
log.Debug().Msgf("Executing pending actions for environment %d", id)
for _, pendingAction := range pendingActions {
if pendingAction.EndpointID == id {
log.Debug().Msgf("executing pending action id=%d, action=%s", pendingAction.ID, pendingAction.Action)
err := service.executePendingAction(pendingAction)
for _, endpointPendingAction := range pendingActions {
if endpointPendingAction.EndpointID == id {
err := service.executePendingAction(endpointPendingAction, endpoint)
if err != nil {
log.Warn().Msgf("failed to execute pending action: %v", err)
return
log.Warn().Err(err).Msgf("failed to execute pending action")
return fmt.Errorf("failed to execute pending action: %w", err)
}
err = service.dataStore.PendingActions().Delete(pendingAction.ID)
err = service.dataStore.PendingActions().Delete(endpointPendingAction.ID)
if err != nil {
log.Warn().Msgf("failed to delete pending action: %v", err)
return
log.Error().Err(err).Msgf("failed to delete pending action")
return fmt.Errorf("failed to delete pending action: %w", err)
}
log.Debug().Msgf("pending action %d finished", pendingAction.ID)
}
}
return nil
}
func (service *PendingActionsService) executePendingAction(pendingAction portainer.PendingAction) error {
func (service *PendingActionsService) executePendingAction(pendingAction portainer.PendingActions, endpoint *portainer.Endpoint) error {
log.Debug().Msgf("Executing pending action %s for environment %d", pendingAction.Action, pendingAction.EndpointID)
defer func() {
if r := recover(); r != nil {
log.Error().Msgf("recovered from panic while executing pending action %s for environment %d: %v", pendingAction.Action, pendingAction.EndpointID, r)
}
log.Debug().Msgf("End executing pending action %s for environment %d", pendingAction.Action, pendingAction.EndpointID)
}()
handler, ok := handlers[pendingAction.Action]
if !ok {
log.Warn().Msgf("no handler found for pending action %s", pendingAction.Action)
switch pendingAction.Action {
case CleanNAPWithOverridePolicies:
if (pendingAction.ActionData == nil) || (pendingAction.ActionData.(portainer.EndpointGroupID) == 0) {
service.authorizationService.CleanNAPWithOverridePolicies(service.dataStore, endpoint, nil)
return nil
}
endpointGroupID := pendingAction.ActionData.(portainer.EndpointGroupID)
endpointGroup, err := service.dataStore.EndpointGroup().Read(portainer.EndpointGroupID(endpointGroupID))
if err != nil {
log.Error().Err(err).Msgf("Error reading environment group to clean NAP with override policies for environment %d and environment group %d", endpoint.ID, endpointGroup.ID)
return fmt.Errorf("failed to retrieve environment group %d: %w", endpointGroupID, err)
}
err = service.authorizationService.CleanNAPWithOverridePolicies(service.dataStore, endpoint, endpointGroup)
if err != nil {
log.Error().Err(err).Msgf("Error cleaning NAP with override policies for environment %d and environment group %d", endpoint.ID, endpointGroup.ID)
return fmt.Errorf("failed to clean NAP with override policies for environment %d and environment group %d: %w", endpoint.ID, endpointGroup.ID, err)
}
return nil
case DeletePortainerK8sRegistrySecrets:
if pendingAction.ActionData == nil {
return nil
}
registryData, err := convertToDeletePortainerK8sRegistrySecretsData(pendingAction.ActionData)
if err != nil {
return fmt.Errorf("failed to parse pendingActionData: %w", err)
}
err = service.DeleteKubernetesRegistrySecrets(endpoint, registryData)
if err != nil {
log.Warn().Err(err).Int("endpoint_id", int(endpoint.ID)).Msgf("Unable to delete kubernetes registry secrets")
return fmt.Errorf("failed to delete kubernetes registry secrets for environment %d: %w", endpoint.ID, err)
}
return nil
}
return handler.Execute(pendingAction)
return nil
}

View File

@@ -6,13 +6,10 @@ import (
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/volume"
gittypes "github.com/portainer/portainer/api/git/types"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/portainer/portainer/pkg/featureflags"
"golang.org/x/oauth2"
v1 "k8s.io/api/core/v1"
)
@@ -245,8 +242,8 @@ type (
Containers []DockerContainerSnapshot `json:"Containers" swaggerignore:"true"`
Volumes volume.ListResponse `json:"Volumes" swaggerignore:"true"`
Networks []types.NetworkResource `json:"Networks" swaggerignore:"true"`
Images []image.Summary `json:"Images" swaggerignore:"true"`
Info system.Info `json:"Info" swaggerignore:"true"`
Images []types.ImageSummary `json:"Images" swaggerignore:"true"`
Info types.Info `json:"Info" swaggerignore:"true"`
Version types.Version `json:"Version" swaggerignore:"true"`
}
@@ -381,6 +378,15 @@ type (
// EdgeStackStatusType represents an edge stack status type
EdgeStackStatusType int
PendingActionsID int
PendingActions struct {
ID PendingActionsID `json:"ID"`
EndpointID EndpointID `json:"EndpointID"`
Action string `json:"Action"`
ActionData interface{} `json:"ActionData"`
CreatedAt int64 `json:"CreatedAt"`
}
// Environment(Endpoint) represents a Docker environment(endpoint) with all the info required
// to connect to it
Endpoint struct {
@@ -750,20 +756,19 @@ type (
// OAuthSettings represents the settings used to authorize with an authorization server
OAuthSettings struct {
ClientID string `json:"ClientID"`
ClientSecret string `json:"ClientSecret,omitempty"`
AccessTokenURI string `json:"AccessTokenURI"`
AuthorizationURI string `json:"AuthorizationURI"`
ResourceURI string `json:"ResourceURI"`
RedirectURI string `json:"RedirectURI"`
UserIdentifier string `json:"UserIdentifier"`
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
KubeSecretKey []byte `json:"KubeSecretKey"`
AuthStyle oauth2.AuthStyle `json:"AuthStyle"`
ClientID string `json:"ClientID"`
ClientSecret string `json:"ClientSecret,omitempty"`
AccessTokenURI string `json:"AccessTokenURI"`
AuthorizationURI string `json:"AuthorizationURI"`
ResourceURI string `json:"ResourceURI"`
RedirectURI string `json:"RedirectURI"`
UserIdentifier string `json:"UserIdentifier"`
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
KubeSecretKey []byte `json:"KubeSecretKey"`
}
// Pair defines a key/value string pair
@@ -1590,7 +1595,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.22.0"
APIVersion = "2.20.0"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
@@ -1719,8 +1724,6 @@ const (
EdgeStackStatusRollingBack
// EdgeStackStatusRolledBack represents an Edge stack which has rolled back
EdgeStackStatusRolledBack
// EdgeStackStatusCompleted represents a completed Edge stack
EdgeStackStatusCompleted
)
const (

View File

@@ -138,14 +138,13 @@ func agentServer(t *testing.T) string {
Handler: h,
}
errCh := make(chan error)
go func() {
errCh <- s.Serve(l)
err := s.Serve(l)
require.ErrorIs(t, err, http.ErrServerClosed)
}()
t.Cleanup(func() {
require.NoError(t, s.Shutdown(context.Background()))
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
s.Shutdown(context.Background())
})
return "http://" + l.Addr().String()

View File

@@ -16,7 +16,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/pkg/errors"
@@ -25,7 +24,7 @@ import (
)
const (
defaultUnpackerImage = "portainer/compose-unpacker:" + portainer.APIVersion
defaultUnpackerImage = "portainer/compose-unpacker:latest"
composeUnpackerImageEnvVar = "COMPOSE_UNPACKER_IMAGE"
composePathPrefix = "portainer-compose-unpacker"
)
@@ -212,9 +211,9 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
if err != nil {
return errors.Wrap(err, "unable to create unpacker container")
}
defer cli.ContainerRemove(ctx, unpackerContainer.ID, container.RemoveOptions{})
defer cli.ContainerRemove(ctx, unpackerContainer.ID, types.ContainerRemoveOptions{})
if err := cli.ContainerStart(ctx, unpackerContainer.ID, container.StartOptions{}); err != nil {
if err := cli.ContainerStart(ctx, unpackerContainer.ID, types.ContainerStartOptions{}); err != nil {
return errors.Wrap(err, "start unpacker container error")
}
@@ -229,7 +228,7 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
stdErr := &bytes.Buffer{}
out, err := cli.ContainerLogs(ctx, unpackerContainer.ID, container.LogsOptions{ShowStdout: true, ShowStderr: true})
out, err := cli.ContainerLogs(ctx, unpackerContainer.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
if err != nil {
log.Error().Err(err).Msg("unable to get logs from unpacker container")
} else {
@@ -336,6 +335,6 @@ func getTargetSocketBind(osType string) string {
// Per https://stackoverflow.com/a/50590287 and Docker's LocalNodeState possible values
// `LocalNodeStateInactive` means the node is not in a swarm cluster
func isNotInASwarm(info *system.Info) bool {
func isNotInASwarm(info *types.Info) bool {
return info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive
}

View File

@@ -48,11 +48,11 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
return errors.New("pid host disabled for non administrator users")
}
if !securitySettings.AllowDeviceMappingForRegularUsers && len(service.Devices) > 0 {
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
return errors.New("device mapping disabled for non administrator users")
}
if !securitySettings.AllowSysctlSettingForRegularUsers && len(service.Sysctls) > 0 {
if !securitySettings.AllowSysctlSettingForRegularUsers && service.Sysctls != nil && len(service.Sysctls) > 0 {
return errors.New("sysctl setting disabled for non administrator users")
}

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