Compare commits

...

8 Commits

Author SHA1 Message Date
Steven Kang 8332232840 chore: version bump 2.33.1 (#1108) 2025-08-27 11:30:12 +12:00
andres-portainer 816a6f9bef chore(bbolt): upgrade bbolt to v1.4.3 BE-12193 (#1104) 2025-08-25 17:59:33 -03:00
Devon Steenberg e86ea22900 fix(sslflags): Deprecate ssl flags [BE-12168] (#1076) 2025-08-25 20:25:07 +12:00
Malcolm Lockyer 12b2acbc00 fix(standard): manual endpoint refresh fails to save new status [be-12188] (#1096) 2025-08-25 13:49:04 +12:00
Ali 4a8b42928e fix(environments): create k8s specific edge agent before connecting [r8s-438] (#1086)
Merging because this change is unrelated to the failing kubernetes/tests/helm-oci.spec.ts tests
2025-08-25 09:32:16 +12:00
Oscar Zhou 2e828b39da fix(autoupdate): update tooltips in edge stack gitops update [BE-12177] (#1080) 2025-08-23 10:55:57 +12:00
Steven Kang 49c6521c23 fix: GHSA-2464-8j7c-4cjm - release 2.33 [R8S-495] (#1089) 2025-08-22 14:03:16 +12:00
Steven Kang debf1a742b chore: version bump 2.33.0 (#1065) 2025-08-20 11:28:05 +12:00
20 changed files with 685 additions and 44 deletions
+65 -12
View File
@@ -9,8 +9,8 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/alecthomas/kingpin/v2"
"github.com/rs/zerolog/log"
"gopkg.in/alecthomas/kingpin.v2"
)
// Service implements the CLIService interface
@@ -35,16 +35,9 @@ func CLIFlags() *portainer.CLIFlags {
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database to the previous backup").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
@@ -70,8 +63,37 @@ func CLIFlags() *portainer.CLIFlags {
func (Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
var hasSSLFlag, hasSSLCertFlag, hasSSLKeyFlag bool
sslFlag := kingpin.Flag(
"ssl",
"Secure Portainer instance using SSL (deprecated)",
).Default(defaultSSL).IsSetByUser(&hasSSLFlag)
ssl := sslFlag.Bool()
sslCertFlag := kingpin.Flag(
"sslcert",
"Path to the SSL certificate used to secure the Portainer instance",
).IsSetByUser(&hasSSLCertFlag)
sslCert := sslCertFlag.String()
sslKeyFlag := kingpin.Flag(
"sslkey",
"Path to the SSL key used to secure the Portainer instance",
).IsSetByUser(&hasSSLKeyFlag)
sslKey := sslKeyFlag.String()
flags := CLIFlags()
var hasTLSFlag, hasTLSCertFlag, hasTLSKeyFlag bool
tlsFlag := kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).IsSetByUser(&hasTLSFlag)
flags.TLS = tlsFlag.Bool()
tlsCertFlag := kingpin.Flag(
"tlscert",
"Path to the TLS certificate file",
).Default(defaultTLSCertPath).IsSetByUser(&hasTLSCertFlag)
flags.TLSCert = tlsCertFlag.String()
tlsKeyFlag := kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).IsSetByUser(&hasTLSKeyFlag)
flags.TLSKey = tlsKeyFlag.String()
flags.TLSCacert = kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String()
kingpin.Parse()
if !filepath.IsAbs(*flags.Assets) {
@@ -83,6 +105,41 @@ func (Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
}
// If the user didn't provide a tls flag remove the defaults to match previous behaviour
if !hasTLSFlag {
if !hasTLSCertFlag {
*flags.TLSCert = ""
}
if !hasTLSKeyFlag {
*flags.TLSKey = ""
}
}
if hasSSLFlag {
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslFlag.Model().Name, tlsFlag.Model().Name)
if !hasTLSFlag {
flags.TLS = ssl
}
}
if hasSSLCertFlag {
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslCertFlag.Model().Name, tlsCertFlag.Model().Name)
if !hasTLSCertFlag {
flags.TLSCert = sslCert
}
}
if hasSSLKeyFlag {
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslKeyFlag.Model().Name, tlsKeyFlag.Model().Name)
if !hasTLSKeyFlag {
flags.TLSKey = sslKey
}
}
return flags, nil
}
@@ -109,10 +166,6 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.NoAnalytics {
log.Warn().Msg("the --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect")
}
if *flags.SSL {
log.Warn().Msg("SSL is enabled by default and there is no need for the --ssl flag, it has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
}
}
func validateEndpointURL(endpointURL string) error {
+185
View File
@@ -1,9 +1,12 @@
package cli
import (
"io"
"os"
"strings"
"testing"
zerolog "github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
)
@@ -22,3 +25,185 @@ func TestOptionParser(t *testing.T) {
require.False(t, *opts.HTTPDisabled)
require.True(t, *opts.EnableEdgeComputeFeatures)
}
func TestParseTLSFlags(t *testing.T) {
testCases := []struct {
name string
args []string
expectedTLSFlag bool
expectedTLSCertFlag string
expectedTLSKeyFlag string
expectedLogMessages []string
}{
{
name: "no flags",
expectedTLSFlag: false,
expectedTLSCertFlag: "",
expectedTLSKeyFlag: "",
},
{
name: "only ssl flag",
args: []string{
"portainer",
"--ssl",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "",
expectedTLSKeyFlag: "",
},
{
name: "only tls flag",
args: []string{
"portainer",
"--tlsverify",
},
expectedTLSFlag: true,
expectedTLSCertFlag: defaultTLSCertPath,
expectedTLSKeyFlag: defaultTLSKeyPath,
},
{
name: "partial ssl flags",
args: []string{
"portainer",
"--ssl",
"--sslcert=ssl-cert-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "ssl-cert-flag-value",
expectedTLSKeyFlag: "",
},
{
name: "partial tls flags",
args: []string{
"portainer",
"--tlsverify",
"--tlscert=tls-cert-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "tls-cert-flag-value",
expectedTLSKeyFlag: defaultTLSKeyPath,
},
{
name: "partial tls and ssl flags",
args: []string{
"portainer",
"--tlsverify",
"--tlscert=tls-cert-flag-value",
"--sslkey=ssl-key-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "tls-cert-flag-value",
expectedTLSKeyFlag: "ssl-key-flag-value",
},
{
name: "partial tls and ssl flags 2",
args: []string{
"portainer",
"--ssl",
"--tlscert=tls-cert-flag-value",
"--sslkey=ssl-key-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "tls-cert-flag-value",
expectedTLSKeyFlag: "ssl-key-flag-value",
},
{
name: "ssl flags",
args: []string{
"portainer",
"--ssl",
"--sslcert=ssl-cert-flag-value",
"--sslkey=ssl-key-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "ssl-cert-flag-value",
expectedTLSKeyFlag: "ssl-key-flag-value",
expectedLogMessages: []string{
"the \\\"ssl\\\" flag is deprecated. use \\\"tlsverify\\\" instead.",
"the \\\"sslcert\\\" flag is deprecated. use \\\"tlscert\\\" instead.",
"the \\\"sslkey\\\" flag is deprecated. use \\\"tlskey\\\" instead.",
},
},
{
name: "tls flags",
args: []string{
"portainer",
"--tlsverify",
"--tlscert=tls-cert-flag-value",
"--tlskey=tls-key-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "tls-cert-flag-value",
expectedTLSKeyFlag: "tls-key-flag-value",
},
{
name: "tls and ssl flags",
args: []string{
"portainer",
"--tlsverify",
"--tlscert=tls-cert-flag-value",
"--tlskey=tls-key-flag-value",
"--ssl",
"--sslcert=ssl-cert-flag-value",
"--sslkey=ssl-key-flag-value",
},
expectedTLSFlag: true,
expectedTLSCertFlag: "tls-cert-flag-value",
expectedTLSKeyFlag: "tls-key-flag-value",
expectedLogMessages: []string{
"the \\\"ssl\\\" flag is deprecated. use \\\"tlsverify\\\" instead.",
"the \\\"sslcert\\\" flag is deprecated. use \\\"tlscert\\\" instead.",
"the \\\"sslkey\\\" flag is deprecated. use \\\"tlskey\\\" instead.",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var logOutput strings.Builder
setupLogOutput(t, &logOutput)
if tc.args == nil {
tc.args = []string{"portainer"}
}
setOsArgs(t, tc.args)
s := Service{}
flags, err := s.ParseFlags("test-version")
if err != nil {
t.Fatalf("error parsing flags: %v", err)
}
if flags.TLS == nil {
t.Fatal("TLS flag was nil")
}
require.Equal(t, tc.expectedTLSFlag, *flags.TLS, "tlsverify flag didn't match")
require.Equal(t, tc.expectedTLSCertFlag, *flags.TLSCert, "tlscert flag didn't match")
require.Equal(t, tc.expectedTLSKeyFlag, *flags.TLSKey, "tlskey flag didn't match")
for _, expectedLogMessage := range tc.expectedLogMessages {
require.Contains(t, logOutput.String(), expectedLogMessage, "Log didn't contain expected message")
}
})
}
}
func setOsArgs(t *testing.T, args []string) {
t.Helper()
previousArgs := os.Args
os.Args = args
t.Cleanup(func() {
os.Args = previousArgs
})
}
func setupLogOutput(t *testing.T, w io.Writer) {
t.Helper()
oldLogger := zerolog.Logger
zerolog.Logger = zerolog.Output(w)
t.Cleanup(func() {
zerolog.Logger = oldLogger
})
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/alecthomas/kingpin/v2"
)
type pairList []portainer.Pair
+1 -1
View File
@@ -408,7 +408,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
edgeStacksService := edgestacks.NewService(dataStore)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.TLSCert, *flags.TLSKey, fileService, dataStore, shutdownTrigger)
if err != nil {
log.Fatal().Err(err).Msg("")
}
+1 -1
View File
@@ -256,7 +256,7 @@ func (m *Migrator) initMigrations() {
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
m.addMigrations("2.33.0-rc2", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
m.addMigrations("2.33.1", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
// Add new migrations above...
// One function per migration, each versions migration funcs in the same file.
@@ -615,7 +615,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.33.0-rc2",
"KubectlShellImage": "portainer/kubectl-shell:2.33.1",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -944,7 +944,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.33.0-rc2\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.33.1\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}
+10 -4
View File
@@ -372,10 +372,16 @@ func (handler *Handler) createEdgeAgentEndpoint(tx dataservices.DataStoreTx, pay
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: portainerHost,
Type: func() portainer.EndpointType {
// an empty container engine means that the endpoint is a Kubernetes endpoint
if payload.ContainerEngine == "" {
return portainer.EdgeAgentOnKubernetesEnvironment
}
return portainer.EdgeAgentOnDockerEnvironment
}(),
ContainerEngine: payload.ContainerEngine,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
@@ -0,0 +1,172 @@
package endpoints
import (
"net/http"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/pkg/fips"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// EE-only kubeconfig validation tests removed for CE
func TestSaveEndpointAndUpdateAuthorizations(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, false)
endpointGroup := &portainer.EndpointGroup{
ID: 1,
Name: "test-endpoint-group",
}
err := store.EndpointGroup().Create(endpointGroup)
require.NoError(t, err)
h := &Handler{
DataStore: store,
}
testCases := []struct {
name string
endpointType portainer.EndpointType
expectRelation bool
}{
{
name: "create azure environment, expect no relation to be created",
endpointType: portainer.AzureEnvironment,
expectRelation: false,
},
{
name: "create edge agent environment, expect relation to be created",
endpointType: portainer.EdgeAgentOnDockerEnvironment,
expectRelation: true,
},
{
name: "create kubernetes environment, expect no relation to be created",
endpointType: portainer.KubernetesLocalEnvironment,
expectRelation: false,
},
{
name: "create kubeconfig environment, expect no relation to be created",
endpointType: portainer.AgentOnKubernetesEnvironment,
expectRelation: false,
},
{
name: "create agent docker environment, expect no relation to be created",
endpointType: portainer.AgentOnDockerEnvironment,
expectRelation: false,
},
{
name: "create unsecured environment, expect no relation to be created",
endpointType: portainer.DockerEnvironment,
expectRelation: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(store.Endpoint().GetNextIdentifier()),
Type: testCase.endpointType,
GroupID: portainer.EndpointGroupID(endpointGroup.ID),
}
err := h.saveEndpointAndUpdateAuthorizations(store, endpoint)
require.NoError(t, err)
relation, relationErr := store.EndpointRelation().EndpointRelation(endpoint.ID)
if testCase.expectRelation {
require.NoError(t, relationErr)
require.NotNil(t, relation)
} else {
require.Error(t, relationErr)
require.True(t, store.IsErrObjectNotFound(relationErr))
require.Nil(t, relation)
}
})
}
}
func TestCreateEndpointFailure(t *testing.T) {
fips.InitFIPS(false)
_, store := datastore.MustNewTestStore(t, true, false)
h := NewHandler(testhelpers.NewTestRequestBouncer())
h.DataStore = store
payload := &endpointCreatePayload{
Name: "Test Endpoint",
EndpointCreationType: agentEnvironment,
TLS: true,
TLSCertFile: []byte("invalid data"),
TLSKeyFile: []byte("invalid data"),
}
endpoint, httpErr := h.createEndpoint(store, payload)
require.NotNil(t, httpErr)
require.Equal(t, http.StatusInternalServerError, httpErr.StatusCode)
require.Nil(t, endpoint)
}
func TestCreateEdgeAgentEndpoint_ContainerEngineMapping(t *testing.T) {
fips.InitFIPS(false)
_, store := datastore.MustNewTestStore(t, true, false)
// required group for save flow
endpointGroup := &portainer.EndpointGroup{ID: 1, Name: "test-group"}
err := store.EndpointGroup().Create(endpointGroup)
require.NoError(t, err)
h := &Handler{
DataStore: store,
ReverseTunnelService: chisel.NewService(store, nil, nil),
}
tests := []struct {
name string
engine string
wantType portainer.EndpointType
}{
{
name: "empty engine -> EdgeAgentOnKubernetesEnvironment",
engine: "",
wantType: portainer.EdgeAgentOnKubernetesEnvironment,
},
{
name: "docker engine -> EdgeAgentOnDockerEnvironment",
engine: portainer.ContainerEngineDocker,
wantType: portainer.EdgeAgentOnDockerEnvironment,
},
{
name: "podman engine -> EdgeAgentOnDockerEnvironment",
engine: portainer.ContainerEnginePodman,
wantType: portainer.EdgeAgentOnDockerEnvironment,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload := &endpointCreatePayload{
Name: "edge-endpoint",
EndpointCreationType: edgeAgentEnvironment,
ContainerEngine: tc.engine,
GroupID: 1,
URL: "https://portainer.example:9443",
}
ep, httpErr := h.createEdgeAgentEndpoint(store, payload)
require.Nil(t, httpErr)
require.NotNil(t, ep)
assert.Equal(t, tc.wantType, ep.Type)
assert.Equal(t, tc.engine, ep.ContainerEngine)
})
}
}
@@ -49,7 +49,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
continue
}
endpoint.Status = portainer.EndpointStatusUp
latestEndpointReference.Status = portainer.EndpointStatusUp
if snapshotError != nil {
log.Debug().
Str("endpoint", endpoint.Name).
@@ -57,7 +57,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
Err(snapshotError).
Msg("background schedule error (environment snapshot), unable to create snapshot")
endpoint.Status = portainer.EndpointStatusDown
latestEndpointReference.Status = portainer.EndpointStatusDown
}
latestEndpointReference.Agent.Version = endpoint.Agent.Version
@@ -0,0 +1,107 @@
package endpoints
import (
"errors"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_endpointSnapshots(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
endpointID := portainer.EndpointID(123)
endpoint := &portainer.Endpoint{
ID: endpointID,
Name: "mock",
URL: "http://mock.example/",
Status: portainer.EndpointStatusDown, // starts in down state
}
err := store.Endpoint().Create(endpoint)
require.NoError(t, err, "error creating environment")
err = store.User().Create(
&portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
},
)
require.NoError(t, err, "error creating a user")
bouncer := testhelpers.NewTestRequestBouncer()
snapshotService := &mockSnapshotService{
snapshotEndpointShouldSucceed: atomic.Bool{},
}
snapshotService.snapshotEndpointShouldSucceed.Store(true)
h := NewHandler(bouncer)
h.DataStore = store
h.SnapshotService = snapshotService
doPostRequest := func() {
req := httptest.NewRequest(http.MethodPost, "/endpoints/snapshot", nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
testhelpers.AddTestSecurityCookie(req, "Bearer dummytoken")
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
require.Equal(t, http.StatusNoContent, rr.Code, "Status should be 204")
_, err := io.ReadAll(rr.Body)
require.NoError(t, err, "ReadAll should not return error")
}
doPostRequest()
// check that the endpoint has been immediately set to up
endpoint, err = store.Endpoint().Endpoint(endpointID)
require.NoError(t, err, "error getting endpoint")
assert.Equal(t, portainer.EndpointStatusUp, endpoint.Status, "endpoint should be up (1) since mock snapshot returned ok")
// set the mock to return an error
snapshotService.snapshotEndpointShouldSucceed.Store(false)
doPostRequest()
// check that the endpoint has been immediately set to down
endpoint, err = store.Endpoint().Endpoint(endpointID)
require.NoError(t, err, "error getting endpoint")
assert.Equal(t, portainer.EndpointStatusDown, endpoint.Status, "endpoint should be down (2) since mock snapshot returned error")
}
var _ portainer.SnapshotService = &mockSnapshotService{}
type mockSnapshotService struct {
snapshotEndpointShouldSucceed atomic.Bool
}
func (s *mockSnapshotService) Start() {
}
func (s *mockSnapshotService) SetSnapshotInterval(snapshotInterval string) error {
return nil
}
func (s *mockSnapshotService) SnapshotEndpoint(endpoint *portainer.Endpoint) error {
if s.snapshotEndpointShouldSucceed.Load() {
return nil
}
return errors.New("snapshot failed")
}
func (s *mockSnapshotService) FillSnapshotData(endpoint *portainer.Endpoint, includeRaw bool) error {
return nil
}
+1 -1
View File
@@ -81,7 +81,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.33.0-rc2
// @version 2.33.1
// @description.markdown api-description.md
// @termsOfService
+2 -4
View File
@@ -122,14 +122,12 @@ type (
Templates *string
TLS *bool
TLSSkipVerify *bool
HasTLSCacert *bool
TLSCacert *string
TLSCert *string
TLSKey *string
HTTPDisabled *bool
HTTPEnabled *bool
SSL *bool
SSLCert *string
SSLKey *string
Rollback *bool
SnapshotInterval *string
BaseURL *string
@@ -1783,7 +1781,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.33.0-rc2"
APIVersion = "2.33.1"
// Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support)
APIVersionSupport = "LTS"
// Edition is what this edition of Portainer is called
@@ -57,8 +57,10 @@
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
<span class="small text-muted vertical-center">
<pr-icon icon="'wrench'" mode="'primary'"></pr-icon>
You should configure the features available in this Kubernetes environment in the
<a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
<div>
You should configure the features available in this Kubernetes environment in the
<a ui-sref="kubernetes.cluster.setup({endpointId: endpoint.Id})">Kubernetes configuration</a> view.
</div>
</span>
</information-panel>
</div>
@@ -205,6 +205,8 @@ export enum EnvironmentCreationTypes {
export enum ContainerEngine {
Docker = 'docker',
Podman = 'podman',
// an empty container engine means that the endpoint is a Kubernetes endpoint
Kubernetes = '',
}
export enum PlatformType {
@@ -0,0 +1,94 @@
import { HttpResponse } from 'msw';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { server, http } from '@/setup-tests/server';
import { WizardKubernetes } from './WizardKubernetes';
function renderComponent() {
// minimal settings so EdgeAgentForm can render
server.use(
http.get('/api/settings', () =>
HttpResponse.json({
AgentSecret: 'secret',
EdgePortainerUrl: 'https://example.com',
Edge: {
PingInterval: 60,
SnapshotInterval: 60,
CommandInterval: 60,
AsyncMode: false,
TunnelServerAddress: 'portainer.test:8000',
},
})
),
http.get('/api/custom_templates', () => HttpResponse.json([])),
http.get('/api/system/status', () =>
HttpResponse.json({ Version: '2.19.0', Edition: 'CE', InstanceID: '1' })
),
http.get('/api/endpoints', () =>
HttpResponse.json([], {
headers: {
'x-total-available': '0',
'x-total-count': '0',
},
})
)
);
const Wrapped = withTestQueryProvider(() => (
<WizardKubernetes onCreate={() => {}} />
));
return render(<Wrapped />);
}
describe('WizardKubernetes', () => {
test('renders Edge Agent Standard form when selected', async () => {
const { getByText, queryByTestId, findByTestId } = renderComponent();
// select Edge Agent Standard
await userEvent.click(getByText('Edge Agent Standard'));
// verify submit button is visible (smallest sanity check for setup)
await expect(
findByTestId('edge-agent-form-submit-button')
).resolves.toBeVisible();
expect(
queryByTestId('endpointCreate-portainerServerUrlInput')
).toBeInTheDocument();
});
test('submits ContainerEngine as empty string for Kubernetes', async () => {
let observedEntries: Array<[string, string]> = [];
server.use(
http.post('/api/endpoints', async ({ request }) => {
const form = await request.formData();
observedEntries = Array.from(form.entries()).map(([key, value]) => [
key,
typeof value === 'string' ? value : 'binary',
]);
return HttpResponse.json({});
})
);
const { getByText, getByTestId, findByTestId } = renderComponent();
await userEvent.click(getByText('Edge Agent Standard'));
await userEvent.type(getByTestId('environmentCreate-nameInput'), 'k8s-env');
const submitBtn = await findByTestId('edge-agent-form-submit-button');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await userEvent.click(submitBtn);
// assert POST happened and ContainerEngine key exists with empty string
await waitFor(() => {
expect(observedEntries.length).toBeGreaterThan(0);
expect(
observedEntries.some(([k, v]) => k === 'ContainerEngine' && v === '')
).toBe(true);
});
});
});
@@ -2,7 +2,10 @@ import { useState } from 'react';
import { Zap, UploadCloud } from 'lucide-react';
import _ from 'lodash';
import { Environment } from '@/react/portainer/environments/types';
import {
ContainerEngine,
Environment,
} from '@/react/portainer/environments/types';
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
@@ -98,6 +101,7 @@ export function WizardKubernetes({ onCreate }: Props) {
onCreate(environment, 'kubernetesEdgeAgentStandard')
}
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
containerEngine={ContainerEngine.Kubernetes}
/>
);
case 'edgeAgentAsync':
@@ -108,6 +112,7 @@ export function WizardKubernetes({ onCreate }: Props) {
onCreate(environment, 'kubernetesEdgeAgentAsync')
}
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
containerEngine={ContainerEngine.Kubernetes}
/>
);
case 'kubeconfig':
@@ -77,7 +77,7 @@ export function AutoUpdateSettings({
checked={value.ForcePullImage || false}
label="Re-pull image"
labelClass="col-sm-3 col-lg-2"
tooltip="If enabled, then when redeploy is triggered via the webhook or polling, if there's a newer image with the tag that you've specified (e.g. changeable development builds), it's pulled and redeployed. If you haven't specified a tag, or have specified 'latest' as the tag, then the image with the tag 'latest' is pulled and redeployed."
tooltip="If enabled, then when redeploy is triggered via the webhook or polling, if there's a newer image with the tag that you've specified (e.g. changeable development builds), it's pulled and redeployed. If you haven't specified a tag, or have specified 'latest' as the tag, then the image with the tag 'latest' is pulled and redeployed. With relative path enabled, it also redeploys when mounted files (not just the compose file) change."
onChange={(value) => onChange({ ForcePullImage: value })}
/>
</div>
@@ -107,7 +107,22 @@ export function AutoUpdateSettings({
the cluster being overwritten.
</p>
</>
) : undefined
) : (
<p>
If enabled, then when redeploy is triggered via the webhook or
polling, the stack behavior depends on the stack type:
<br />
<strong>Regular stacks:</strong> Redeploy whenever triggered,
without checking for docker-compose file changes
<br />
<strong>Edge stacks:</strong> Redeploy only when the
docker-compose file in the Git repository has changed. Commits
that change unrelated files or mounted files (via relative paths)
do not trigger redeployment. Currently, this option does not
change the redeployment behavior, and it remains a temporary
solution until a more complete behavior is added later.
</p>
)
}
/>
</>
+4 -5
View File
@@ -7,6 +7,7 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/RoaringBitmap/roaring/v2 v2.5.0
github.com/VictoriaMetrics/fastcache v1.12.0
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1
@@ -48,13 +49,12 @@ require (
github.com/stretchr/testify v1.10.0
github.com/urfave/negroni v1.0.0
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.4.0
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.40.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/mod v0.25.0
golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.16.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.18.5
@@ -86,7 +86,6 @@ require (
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
@@ -157,7 +156,7 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -295,7 +294,7 @@ require (
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.3 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect
+8 -5
View File
@@ -49,9 +49,9 @@ github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF
github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
@@ -312,8 +312,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -787,6 +787,8 @@ github.com/zmap/zlint/v3 v3.6.4 h1:r2kHfRF7mIsxW0IH4Og2iZnrlpCLTZBFjnXy1x/ZnZI=
github.com/zmap/zlint/v3 v3.6.4/go.mod h1:KQLVUquVaO5YJDl5a4k/7RPIbIW2v66+sRoBPNZusI8=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
@@ -955,6 +957,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1015,7 +1019,6 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.33.0-rc2",
"version": "2.33.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"