From 9a3f6b21d25861f5c8e9e8baf99b04e066aa1c22 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 3 Feb 2025 14:20:35 +0100 Subject: [PATCH 001/212] feat(app/service-details): hide view while loading data (#348) --- app/docker/views/services/edit/service.html | 525 +++++++++--------- .../views/services/edit/serviceController.js | 4 + 2 files changed, 270 insertions(+), 259 deletions(-) diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index fc49a3e13..406c487ca 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -1,274 +1,281 @@ -
-
- -
- - - - - - + + + + + +
+
+
+

Container specification

+
+
+
+
+
+
+ +
+
+
+

Networks & ports

+
+ + + +
+
+
+ +
+
+
+

Service specification

+
+
+
+
+
+
+
+
+
+
+
+ +
- - - - - - -
-
-
-

Container specification

-
-
-
-
-
-
-
- -
-
-
-

Networks & ports

-
- - - -
-
-
- -
-
-
-

Service specification

-
-
-
-
-
-
-
-
-
-
-
- -
diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index 69474b398..3344ef2ab 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -731,6 +731,7 @@ angular.module('portainer.docker').controller('ServiceController', [ }; function initView() { + $scope.isLoading = true; var apiVersion = $scope.applicationState.endpoint.apiVersion; var agentProxy = $scope.applicationState.endpoint.mode.agentProxy; @@ -855,6 +856,9 @@ angular.module('portainer.docker').controller('ServiceController', [ $scope.secrets = []; $scope.configs = []; Notifications.error('Failure', err, 'Unable to retrieve service details'); + }) + .finally(() => { + $scope.isLoading = false; }); } From c0d30a455f606409d49d95df0e5c221893c2f233 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 3 Feb 2025 20:25:25 +0100 Subject: [PATCH 002/212] fix(api/edge): backend panic on edge stack removal (#371) --- api/http/handler/edgestacks/edgestack_status_update.go | 4 ++-- .../edgestacks/edgestack_status_update_coordinator.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/http/handler/edgestacks/edgestack_status_update.go b/api/http/handler/edgestacks/edgestack_status_update.go index 4cf912030..fef5a6927 100644 --- a/api/http/handler/edgestacks/edgestack_status_update.go +++ b/api/http/handler/edgestacks/edgestack_status_update.go @@ -79,7 +79,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req } updateFn := func(stack *portainer.EdgeStack) (*portainer.EdgeStack, error) { - return handler.updateEdgeStackStatus(stack, endpoint, r, stack.ID, payload) + return handler.updateEdgeStackStatus(stack, stack.ID, payload) } stack, err := handler.stackCoordinator.UpdateStatus(r, portainer.EdgeStackID(stackID), updateFn) @@ -99,7 +99,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req return response.JSON(w, stack) } -func (handler *Handler) updateEdgeStackStatus(stack *portainer.EdgeStack, endpoint *portainer.Endpoint, r *http.Request, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) { +func (handler *Handler) updateEdgeStackStatus(stack *portainer.EdgeStack, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) { if payload.Version > 0 && payload.Version < stack.Version { return stack, nil } diff --git a/api/http/handler/edgestacks/edgestack_status_update_coordinator.go b/api/http/handler/edgestacks/edgestack_status_update_coordinator.go index f81e2fc3e..885b4c6da 100644 --- a/api/http/handler/edgestacks/edgestack_status_update_coordinator.go +++ b/api/http/handler/edgestacks/edgestack_status_update_coordinator.go @@ -60,6 +60,11 @@ func (c *EdgeStackStatusUpdateCoordinator) loop() { return err } + // Return early when the agent tries to update the status on a deleted stack + if stack == nil { + return nil + } + // 2. Mutate the edge stack opportunistically until there are no more pending updates for { stack, err = u.updateFn(stack) From a50a9c56176eb853f122728529150b1a61faa51c Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 3 Feb 2025 20:50:24 +0100 Subject: [PATCH 003/212] fix(app/edge): edge stacks webhooks cannot be disabled once created (#372) --- .../edge-stacks/ItemView/EditEdgeStackForm/NonGitStackForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/NonGitStackForm.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/NonGitStackForm.tsx index d0d1e5373..5d7956822 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/NonGitStackForm.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/NonGitStackForm.tsx @@ -143,7 +143,7 @@ export function NonGitStackForm({ edgeStack }: { edgeStack: EdgeStack }) { updateVersion, webhook: values.webhookEnabled ? edgeStack.Webhook || createWebhookId() - : undefined, + : '', envVars: values.envVars, rollbackTo: values.rollbackTo, staggerConfig: values.staggerConfig, From 379711951c341af366abed2041bd506fa4134bc9 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:41:24 +1300 Subject: [PATCH 004/212] fix(edgegroup): failed to associate env to static edge group [BE-11599] (#368) --- api/http/handler/edgegroups/edgegroup_update.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index 2d04f7cd2..e67607d92 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -167,7 +167,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error { relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID) - if err != nil { + if err != nil && !handler.DataStore.IsErrObjectNotFound(err) { return err } @@ -183,6 +183,11 @@ func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoi edgeStackSet[edgeStackID] = true } + if relation == nil { + relation = &portainer.EndpointRelation{ + EndpointID: endpoint.ID, + } + } relation.EdgeStacks = edgeStackSet return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation) From 5af0859f67e5cff76399277ee5769411f14f42f3 Mon Sep 17 00:00:00 2001 From: James Player Date: Tue, 4 Feb 2025 15:34:33 +1300 Subject: [PATCH 005/212] fix(datatables): "Select all" should select only elements of the current page (#376) --- .../components/datatables/Datatable.test.tsx | 44 +++++++++++++++++++ .../components/datatables/select-column.tsx | 13 ++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/react/components/datatables/Datatable.test.tsx b/app/react/components/datatables/Datatable.test.tsx index 25e1ed1eb..3b5a1e63f 100644 --- a/app/react/components/datatables/Datatable.test.tsx +++ b/app/react/components/datatables/Datatable.test.tsx @@ -155,6 +155,50 @@ describe('Datatable', () => { expect(screen.getByText('No data available')).toBeInTheDocument(); }); + + it('selects/deselects only page rows when select all is clicked', () => { + render( + + ); + + const selectAllCheckbox = screen.getByLabelText('Select all rows'); + fireEvent.click(selectAllCheckbox); + + // Check if all rows on the page are selected + expect(screen.getByText('2 item(s) selected')).toBeInTheDocument(); + + // Deselect + fireEvent.click(selectAllCheckbox); + const checkboxes: HTMLInputElement[] = screen.queryAllByRole('checkbox'); + expect(checkboxes.filter((checkbox) => checkbox.checked).length).toBe(0); + }); + + it('selects/deselects all rows including other pages when select all is clicked with shift key', () => { + render( + + ); + + const selectAllCheckbox = screen.getByLabelText('Select all rows'); + fireEvent.click(selectAllCheckbox, { shiftKey: true }); + + // Check if all rows on the page are selected + expect(screen.getByText('3 item(s) selected')).toBeInTheDocument(); + + // Deselect + fireEvent.click(selectAllCheckbox, { shiftKey: true }); + const checkboxes: HTMLInputElement[] = screen.queryAllByRole('checkbox'); + expect(checkboxes.filter((checkbox) => checkbox.checked).length).toBe(0); + }); }); // Test the defaultGlobalFilterFn used in searches diff --git a/app/react/components/datatables/select-column.tsx b/app/react/components/datatables/select-column.tsx index 6e5b4a920..f20f4dd0b 100644 --- a/app/react/components/datatables/select-column.tsx +++ b/app/react/components/datatables/select-column.tsx @@ -11,15 +11,22 @@ export function createSelectColumn(dataCy: string): ColumnDef { { + // Select all rows if shift key is held down, otherwise only page rows + if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.shiftKey) { + table.getToggleAllRowsSelectedHandler()(e); + return; + } + table.getToggleAllPageRowsSelectedHandler()(e); + }} disabled={table.getRowModel().rows.every((row) => !row.getCanSelect())} onClick={(e) => { e.stopPropagation(); }} aria-label="Select all rows" - title="Select all rows" + title="Select all rows. Hold shift key to select across all pages." /> ), cell: ({ row, table }) => ( From bc19d6592fad172faaacdd0b8a42a17c35cc5daf Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:17:51 +1300 Subject: [PATCH 006/212] fix(libstack): cannot open std edge stack log page [BE-11603] (#384) --- pkg/libstack/compose/composeplugin.go | 6 ++- pkg/libstack/compose/logwriter.go | 58 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 pkg/libstack/compose/logwriter.go diff --git a/pkg/libstack/compose/composeplugin.go b/pkg/libstack/compose/composeplugin.go index 7b6a639c4..3162c6ab7 100644 --- a/pkg/libstack/compose/composeplugin.go +++ b/pkg/libstack/compose/composeplugin.go @@ -31,8 +31,10 @@ const PortainerEdgeStackLabel = "io.portainer.edge_stack_id" var mu sync.Mutex func init() { - // Redirect Compose logging to zerolog - logrus.SetOutput(log.Logger) + logrus.SetOutput(&LogrusToZerologWriter{}) + logrus.SetFormatter(&logrus.TextFormatter{ + DisableTimestamp: true, + }) } func withCli( diff --git a/pkg/libstack/compose/logwriter.go b/pkg/libstack/compose/logwriter.go new file mode 100644 index 000000000..477433552 --- /dev/null +++ b/pkg/libstack/compose/logwriter.go @@ -0,0 +1,58 @@ +package compose + +import ( + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// LogrusToZerologWriter is a custom logrus writer that writes logrus logs to zerolog. +// logrus is the logging library used by Docker Compose. +type LogrusToZerologWriter struct{} + +func (ltzw *LogrusToZerologWriter) Write(p []byte) (n int, err error) { + logMessage := string(p) + logMessage = strings.TrimSuffix(logMessage, "\n") + + // Parse the log level and message from the logrus log. + // This assumes logrus's default text format. + var level, message string + if strings.HasPrefix(logMessage, "time=") { + // Example logrus log: `time="2023-10-01T12:34:56Z" level=info msg="This is a log message" key=value` + parts := strings.SplitN(logMessage, " ", 4) + if len(parts) >= 3 { + level = strings.TrimPrefix(parts[1], "level=") + message = strings.TrimPrefix(parts[2], "msg=") + message = strings.Trim(message, `"`) + } + } else { + // Fallback for simpler log formats. + level = "info" + message = logMessage + } + + // Map logrus levels to zerolog levels. + var zlogLevel zerolog.Level + switch level { + case "debug": + zlogLevel = zerolog.DebugLevel + case "info": + zlogLevel = zerolog.InfoLevel + case "warn", "warning": + zlogLevel = zerolog.WarnLevel + case "error": + zlogLevel = zerolog.ErrorLevel + case "fatal": + zlogLevel = zerolog.FatalLevel + case "panic": + zlogLevel = zerolog.PanicLevel + default: + zlogLevel = zerolog.InfoLevel + } + + // Log the message using zerolog. + log.WithLevel(zlogLevel).Msg(message) + + return len(p), nil +} From 678cd5455332dcac9f9c00eeed0ee36ef448d93c Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Wed, 5 Feb 2025 15:03:39 +1300 Subject: [PATCH 007/212] security: cve-2024-45338 develop (#386) --- binary-version.json | 8 ++++---- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/binary-version.json b/binary-version.json index c91bf37a6..914838f53 100644 --- a/binary-version.json +++ b/binary-version.json @@ -1,6 +1,6 @@ { - "docker": "v27.1.2", - "helm": "v3.16.4", - "kubectl": "v1.31.4", - "mingit": "2.46.0.1" + "docker": "v27.5.1", + "helm": "v3.17.0", + "kubectl": "v1.32.1", + "mingit": "2.47.0.1" } diff --git a/go.mod b/go.mod index cc34f23f2..0bc355d71 100644 --- a/go.mod +++ b/go.mod @@ -242,7 +242,7 @@ require ( go.opentelemetry.io/otel/trace v1.25.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/mock v0.5.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 9a98a81e3..e5855879f 100644 --- a/go.sum +++ b/go.sum @@ -723,8 +723,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 7001f8e088dc1acb736c2ab7de08c458b22cbe81 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:20:20 +1300 Subject: [PATCH 008/212] fix(edge): check all endpoint_relation db query logic [BE-11602] (#378) --- api/http/handler/edgegroups/edgegroup_update.go | 1 + api/http/handler/edgestacks/edgestack_update.go | 13 +++++++++++-- .../endpointedge/endpointedge_status_inspect.go | 3 +++ api/http/handler/endpointgroups/endpoints.go | 12 +++++++++++- api/http/handler/endpoints/update_edge_relations.go | 1 + api/http/handler/tags/tag_delete.go | 10 +++++++++- api/internal/edge/edgestacks/service.go | 12 ++++++++++++ 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index e67607d92..319a1b03b 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -186,6 +186,7 @@ func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoi if relation == nil { relation = &portainer.EndpointRelation{ EndpointID: endpoint.ID, + EdgeStacks: make(map[portainer.EdgeStackID]bool), } } relation.EdgeStacks = edgeStackSet diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index 9b5bb623d..593e403ef 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -107,7 +107,7 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, payload.DeploymentType) if err != nil { - return nil, httperror.BadRequest("unable to check for existence of non fitting environments: %w", err) + return nil, httperror.InternalServerError("unable to check for existence of non fitting environments: %w", err) } if hasWrongType { return nil, httperror.BadRequest("edge stack with config do not match the environment type", nil) @@ -151,6 +151,9 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge for endpointID := range endpointsToRemove { relation, err := tx.EndpointRelation().EndpointRelation(endpointID) if err != nil { + if tx.IsErrObjectNotFound(err) { + continue + } return nil, nil, errors.WithMessage(err, "Unable to find environment relation in database") } @@ -170,10 +173,16 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge for endpointID := range endpointsToAdd { relation, err := tx.EndpointRelation().EndpointRelation(endpointID) - if err != nil { + if err != nil && !tx.IsErrObjectNotFound(err) { return nil, nil, errors.WithMessage(err, "Unable to find environment relation in database") } + if relation == nil { + relation = &portainer.EndpointRelation{ + EndpointID: endpointID, + EdgeStacks: map[portainer.EdgeStackID]bool{}, + } + } relation.EdgeStacks[edgeStackID] = true if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil { diff --git a/api/http/handler/endpointedge/endpointedge_status_inspect.go b/api/http/handler/endpointedge/endpointedge_status_inspect.go index 9bd341561..4d6368493 100644 --- a/api/http/handler/endpointedge/endpointedge_status_inspect.go +++ b/api/http/handler/endpointedge/endpointedge_status_inspect.go @@ -264,6 +264,9 @@ func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID p func (handler *Handler) buildEdgeStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) ([]stackStatusResponse, *httperror.HandlerError) { relation, err := tx.EndpointRelation().EndpointRelation(endpointID) if err != nil { + if tx.IsErrObjectNotFound(err) { + return nil, nil + } return nil, httperror.InternalServerError("Unable to retrieve relation object from the database", err) } diff --git a/api/http/handler/endpointgroups/endpoints.go b/api/http/handler/endpointgroups/endpoints.go index b7c9771bb..b34032d9e 100644 --- a/api/http/handler/endpointgroups/endpoints.go +++ b/api/http/handler/endpointgroups/endpoints.go @@ -21,10 +21,17 @@ func (handler *Handler) updateEndpointRelations(tx dataservices.DataStoreTx, end } endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID) - if err != nil { + if err != nil && !tx.IsErrObjectNotFound(err) { return err } + if endpointRelation == nil { + endpointRelation = &portainer.EndpointRelation{ + EndpointID: endpoint.ID, + EdgeStacks: make(map[portainer.EdgeStackID]bool), + } + } + edgeGroups, err := tx.EdgeGroup().ReadAll() if err != nil { return err @@ -32,6 +39,9 @@ func (handler *Handler) updateEndpointRelations(tx dataservices.DataStoreTx, end edgeStacks, err := tx.EdgeStack().EdgeStacks() if err != nil { + if tx.IsErrObjectNotFound(err) { + return nil + } return err } diff --git a/api/http/handler/endpoints/update_edge_relations.go b/api/http/handler/endpoints/update_edge_relations.go index 8bebadedd..1390c9fd4 100644 --- a/api/http/handler/endpoints/update_edge_relations.go +++ b/api/http/handler/endpoints/update_edge_relations.go @@ -23,6 +23,7 @@ func (handler *Handler) updateEdgeRelations(tx dataservices.DataStoreTx, endpoin relation = &portainer.EndpointRelation{ EndpointID: endpoint.ID, + EdgeStacks: map[portainer.EdgeStackID]bool{}, } if err := tx.EndpointRelation().Create(relation); err != nil { return errors.WithMessage(err, "Unable to create environment relation inside the database") diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index ad8ef1347..4f8554faf 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -133,10 +133,17 @@ func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error { func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error { endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID) - if err != nil { + if err != nil && !tx.IsErrObjectNotFound(err) { return err } + if endpointRelation == nil { + endpointRelation = &portainer.EndpointRelation{ + EndpointID: endpoint.ID, + EdgeStacks: make(map[portainer.EdgeStackID]bool), + } + } + endpointGroup, err := tx.EndpointGroup().Read(endpoint.GroupID) if err != nil { return err @@ -147,6 +154,7 @@ func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.End for _, edgeStackID := range endpointStacks { stacksSet[edgeStackID] = true } + endpointRelation.EdgeStacks = stacksSet return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation) diff --git a/api/internal/edge/edgestacks/service.go b/api/internal/edge/edgestacks/service.go index 4fc6943af..3a19e8c9d 100644 --- a/api/internal/edge/edgestacks/service.go +++ b/api/internal/edge/edgestacks/service.go @@ -11,6 +11,7 @@ import ( httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/internal/edge" edgetypes "github.com/portainer/portainer/api/internal/edge/types" + "github.com/rs/zerolog/log" "github.com/pkg/errors" ) @@ -119,6 +120,9 @@ func (service *Service) updateEndpointRelations(tx dataservices.DataStoreTx, edg for _, endpointID := range relatedEndpointIds { relation, err := endpointRelationService.EndpointRelation(endpointID) if err != nil { + if tx.IsErrObjectNotFound(err) { + continue + } return fmt.Errorf("unable to find endpoint relation in database: %w", err) } @@ -147,6 +151,14 @@ func (service *Service) DeleteEdgeStack(tx dataservices.DataStoreTx, edgeStackID for _, endpointID := range relatedEndpointIds { relation, err := tx.EndpointRelation().EndpointRelation(endpointID) if err != nil { + if tx.IsErrObjectNotFound(err) { + log.Warn(). + Int("endpoint_id", int(endpointID)). + Msg("Unable to find endpoint relation in database, skipping") + + continue + } + return errors.WithMessage(err, "Unable to find environment relation in database") } From 5423a2f1b95a1daa118093d7c8408db2ca2af302 Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Wed, 5 Feb 2025 15:56:30 +1300 Subject: [PATCH 009/212] security: cve-2025-21613 develop (#390) --- go.mod | 12 ++++++------ go.sum | 55 ++++++++++++++++++------------------------------------- 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index 0bc355d71..053035ec1 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/docker/docker v27.4.0+incompatible github.com/fvbommel/sortorder v1.1.0 github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 - github.com/go-git/go-git/v5 v5.11.0 + github.com/go-git/go-git/v5 v5.13.0 github.com/go-ldap/ldap/v3 v3.4.1 github.com/go-playground/validator/v10 v10.12.0 github.com/gofrs/uuid v4.2.0+incompatible @@ -71,7 +71,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/ProtonMail/go-crypto v1.1.3 // 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 @@ -105,7 +105,7 @@ require ( github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.1.10 // indirect github.com/containers/storage v1.53.0 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/buildx v0.18.0 // indirect @@ -125,7 +125,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-billy/v5 v5.6.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect @@ -205,10 +205,10 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect github.com/segmentio/asm v1.1.3 // indirect - github.com/sergi/go-diff v1.3.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index e5855879f..6427b69de 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9b github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= @@ -99,7 +99,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -110,7 +109,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= @@ -163,8 +161,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -205,8 +203,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNE github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= +github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -227,18 +225,18 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= +github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= +github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU= github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= @@ -486,8 +484,8 @@ github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4 github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -559,8 +557,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ= github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -571,8 +569,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= @@ -697,8 +695,6 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= @@ -706,7 +702,6 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -720,9 +715,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= @@ -735,7 +727,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -764,18 +755,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -783,8 +767,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= @@ -794,7 +776,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 371e84d9a559233d3a2edf59b7ebfa6c13d49c2c Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:22:33 +1300 Subject: [PATCH 010/212] fix(podman): create new image from a container in podman [r8s-90] (#347) --- app/docker/helpers/imageHelper.js | 8 ++++++-- .../containers/edit/containerController.js | 4 ++-- .../views/images/edit/imageController.js | 4 +--- .../images/import/importImageController.js | 4 +--- app/react/docker/images/utils.test.ts | 18 ++++++++++++++++++ app/react/docker/images/utils.ts | 9 +++++++++ 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js index 0bacd6c53..3ce3d4a45 100644 --- a/app/docker/helpers/imageHelper.js +++ b/app/docker/helpers/imageHelper.js @@ -1,4 +1,4 @@ -import { buildImageFullURIFromModel, imageContainsURL } from '@/react/docker/images/utils'; +import { buildImageFullURIFromModel, imageContainsURL, fullURIIntoRepoAndTag } from '@/react/docker/images/utils'; angular.module('portainer.docker').factory('ImageHelper', ImageHelperFactory); function ImageHelperFactory() { @@ -18,8 +18,12 @@ function ImageHelperFactory() { * @param {PorImageRegistryModel} registry */ function createImageConfigForContainer(imageModel) { + const fromImage = buildImageFullURIFromModel(imageModel); + const { tag, repo } = fullURIIntoRepoAndTag(fromImage); return { - fromImage: buildImageFullURIFromModel(imageModel), + fromImage, + tag, + repo, }; } diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 085b2d73d..78f153e27 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -207,9 +207,9 @@ angular.module('portainer.docker').controller('ContainerController', [ async function commitContainerAsync() { $scope.config.commitInProgress = true; const registryModel = $scope.config.RegistryModel; - const imageConfig = ImageHelper.createImageConfigForContainer(registryModel); + const { repo, tag } = ImageHelper.createImageConfigForContainer(registryModel); try { - await commitContainer(endpoint.Id, { container: $transition$.params().id, repo: imageConfig.fromImage }); + await commitContainer(endpoint.Id, { container: $transition$.params().id, repo, tag }); Notifications.success('Image created', $transition$.params().id); $state.reload(); } catch (err) { diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index 78ae6107c..8ed5c9495 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -2,7 +2,6 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { confirmImageExport } from '@/react/docker/images/common/ConfirmExportModal'; import { confirmDelete } from '@@/modals/confirm'; -import { fullURIIntoRepoAndTag } from '@/react/docker/images/utils'; angular.module('portainer.docker').controller('ImageController', [ '$async', @@ -71,8 +70,7 @@ angular.module('portainer.docker').controller('ImageController', [ $scope.tagImage = function () { const registryModel = $scope.formValues.RegistryModel; - const image = ImageHelper.createImageConfigForContainer(registryModel); - const { repo, tag } = fullURIIntoRepoAndTag(image.fromImage); + const { repo, tag } = ImageHelper.createImageConfigForContainer(registryModel); ImageService.tagImage($transition$.params().id, repo, tag) .then(function success() { diff --git a/app/docker/views/images/import/importImageController.js b/app/docker/views/images/import/importImageController.js index dfdb8ab1a..d5587ecb9 100644 --- a/app/docker/views/images/import/importImageController.js +++ b/app/docker/views/images/import/importImageController.js @@ -1,5 +1,4 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -import { fullURIIntoRepoAndTag } from '@/react/docker/images/utils'; angular.module('portainer.docker').controller('ImportImageController', [ '$scope', @@ -34,8 +33,7 @@ angular.module('portainer.docker').controller('ImportImageController', [ async function tagImage(id) { const registryModel = $scope.formValues.RegistryModel; if (registryModel.Image) { - const image = ImageHelper.createImageConfigForContainer(registryModel); - const { repo, tag } = fullURIIntoRepoAndTag(image.fromImage); + const { repo, tag } = ImageHelper.createImageConfigForContainer(registryModel); try { await ImageService.tagImage(id, repo, tag); } catch (err) { diff --git a/app/react/docker/images/utils.test.ts b/app/react/docker/images/utils.test.ts index 05f0e39ff..70e1cf74c 100644 --- a/app/react/docker/images/utils.test.ts +++ b/app/react/docker/images/utils.test.ts @@ -16,6 +16,24 @@ describe('fullURIIntoRepoAndTag', () => { expect(result).toEqual({ repo: 'nginx', tag: 'latest' }); }); + it('splits image-repo:port/image correctly', () => { + const result = fullURIIntoRepoAndTag('registry.example.com:5000/my-image'); + expect(result).toEqual({ + repo: 'registry.example.com:5000/my-image', + tag: 'latest', + }); + }); + + it('splits image-repo:port/image:tag correctly', () => { + const result = fullURIIntoRepoAndTag( + 'registry.example.com:5000/my-image:v1' + ); + expect(result).toEqual({ + repo: 'registry.example.com:5000/my-image', + tag: 'v1', + }); + }); + it('splits registry:port/image-repo:tag correctly', () => { const result = fullURIIntoRepoAndTag( 'registry.example.com:5000/my-image:v2.1' diff --git a/app/react/docker/images/utils.ts b/app/react/docker/images/utils.ts index 10d6ae0e9..e9aeaf661 100644 --- a/app/react/docker/images/utils.ts +++ b/app/react/docker/images/utils.ts @@ -121,9 +121,18 @@ export function fullURIIntoRepoAndTag(fullURI: string) { // - registry/image-repo:tag // - image-repo:tag // - registry:port/image-repo:tag + // - localhost:5000/nginx // buildImageFullURIFromModel always gives a tag (defaulting to 'latest'), so the tag is always present after the last ':' const parts = fullURI.split(':'); const tag = parts.pop() || 'latest'; + + // handle the case of a repo with a non standard port + if (tag.includes('/')) { + return { + repo: fullURI, + tag: 'latest', + }; + } const repo = parts.join(':'); return { repo, From 9658f757c2c37aa99d88bb047cd0b76400a4fd88 Mon Sep 17 00:00:00 2001 From: viktigpetterr Date: Thu, 6 Feb 2025 18:14:43 +0100 Subject: [PATCH 011/212] fix(endpoints): use the post method for batch delete API operations [BE-11573] (#394) --- api/http/handler/endpoints/endpoint_delete.go | 23 ++++++++++++++++++- api/http/handler/endpoints/handler.go | 5 ++-- .../ListView/useDeleteEnvironmentsMutation.ts | 6 ++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 0add2ccdf..4752364ec 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -91,7 +91,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * // @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria." // @failure 403 "Unauthorized access or operation not allowed." // @failure 500 "Server error occurred while attempting to delete the specified environments." -// @router /endpoints [delete] +// @router /endpoints/delete [post] func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { var p endpointDeleteBatchPayload if err := request.DecodeAndValidateJSONPayload(r, &p); err != nil { @@ -127,6 +127,27 @@ func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Reque return response.Empty(w) } +// @id EndpointDeleteBatchDeprecated +// @summary Remove multiple environments +// @deprecated +// @description Deprecated: use the `POST` endpoint instead. +// @description Remove multiple environments and optionally clean-up associated resources. +// @description **Access policy**: Administrator only. +// @tags endpoints +// @security ApiKeyAuth || jwt +// @accept json +// @produce json +// @param body body endpointDeleteBatchPayload true "List of environments to delete, with optional deleteCluster flag to clean-up associated resources (cloud environments only)" +// @success 204 "Environment(s) successfully deleted." +// @failure 207 {object} endpointDeleteBatchPartialResponse "Partial success. Some environments were deleted successfully, while others failed." +// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria." +// @failure 403 "Unauthorized access or operation not allowed." +// @failure 500 "Server error occurred while attempting to delete the specified environments." +// @router /endpoints [delete] +func (handler *Handler) endpointDeleteBatchDeprecated(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + return handler.endpointDeleteBatch(w, r) +} + func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID portainer.EndpointID, deleteCluster bool) error { endpoint, err := tx.Endpoint().Endpoint(endpointID) if tx.IsErrObjectNotFound(err) { diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index fa852633c..d1e323e8f 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -68,8 +68,8 @@ func NewHandler(bouncer security.BouncerService) *Handler { bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) h.Handle("/endpoints/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete) - h.Handle("/endpoints", - bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteBatch))).Methods(http.MethodDelete) + h.Handle("/endpoints/delete", + bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteBatch))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/dockerhub/{registryId}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet) h.Handle("/endpoints/{id}/snapshot", @@ -85,6 +85,7 @@ func NewHandler(bouncer security.BouncerService) *Handler { // DEPRECATED h.Handle("/endpoints/{id}/status", bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet) + h.Handle("/endpoints", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteBatchDeprecated))).Methods(http.MethodDelete) return h } diff --git a/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts b/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts index 01a87681f..48f539f11 100644 --- a/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts +++ b/app/react/portainer/environments/ListView/useDeleteEnvironmentsMutation.ts @@ -55,11 +55,11 @@ async function deleteEnvironments( environments: { id: EnvironmentId; deleteCluster?: boolean }[] ) { try { - const { data } = await axios.delete<{ + const { data } = await axios.post<{ deleted: EnvironmentId[]; errors: EnvironmentId[]; - } | null>(buildUrl(), { - data: { endpoints: environments }, + } | null>(buildUrl(undefined, 'delete'), { + endpoints: environments, }); return data; } catch (e) { From d4e2b2188ed6c4b72bc28e3a1b821088eae5e07d Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Fri, 7 Feb 2025 08:48:19 +1300 Subject: [PATCH 012/212] chore: bump go version to 1.23.5 develop (#392) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 053035ec1..8f39fe3a9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/portainer/portainer -go 1.23.2 +go 1.23.5 require ( github.com/Masterminds/semver v1.5.0 From 1b2dc6a133ddc4fd300742e46680354a62f8bc8b Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Fri, 7 Feb 2025 14:47:49 +1300 Subject: [PATCH 013/212] version: bump version to 2.27.0-rc2 - develop (#402) --- api/datastore/test_data/output_24_to_latest.json | 4 ++-- api/portainer.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 9fa3e5f09..43d872914 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -610,7 +610,7 @@ "RequiredPasswordLength": 12 }, "KubeconfigExpiry": "0", - "KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc1", + "KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc2", "LDAPSettings": { "AnonymousMode": true, "AutoCreateUsers": true, @@ -943,7 +943,7 @@ } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.27.0-rc1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.27.0-rc2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" }, "webhooks": null } \ No newline at end of file diff --git a/api/portainer.go b/api/portainer.go index ad0989437..64ac47012 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1636,7 +1636,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.27.0-rc1" + APIVersion = "2.27.0-rc2" // 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 From 935c7dd49650727b0ded15016b59a0829f3269e0 Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Mon, 10 Feb 2025 10:45:47 +1300 Subject: [PATCH 014/212] feat: improve diagnostics stability - develop (#355) --- pkg/endpoints/utils.go | 14 ++++++++ pkg/endpoints/utils_test.go | 58 +++++++++++++++++++++++++++++++++ pkg/snapshot/docker_test.go | 1 - pkg/snapshot/kubernetes_test.go | 43 ++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) delete mode 100644 pkg/snapshot/docker_test.go diff --git a/pkg/endpoints/utils.go b/pkg/endpoints/utils.go index e32b56fea..4c704d86f 100644 --- a/pkg/endpoints/utils.go +++ b/pkg/endpoints/utils.go @@ -1,6 +1,7 @@ package endpoints import ( + "github.com/hashicorp/go-version" portainer "github.com/portainer/portainer/api" ) @@ -15,6 +16,11 @@ func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool { return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment } +// IsStandardEdgeEndpoint returns true if this is a standard Edge endpoint and not in async mode on either Docker or Kubernetes +func IsStandardEdgeEndpoint(endpoint *portainer.Endpoint) bool { + return (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && !endpoint.Edge.AsyncMode +} + // IsAssociatedEdgeEndpoint returns true if the environment is an Edge environment // and has a set EdgeID and UserTrusted is true. func IsAssociatedEdgeEndpoint(endpoint *portainer.Endpoint) bool { @@ -26,3 +32,11 @@ func IsAssociatedEdgeEndpoint(endpoint *portainer.Endpoint) bool { func HasDirectConnectivity(endpoint *portainer.Endpoint) bool { return !IsEdgeEndpoint(endpoint) || (IsAssociatedEdgeEndpoint(endpoint) && !endpoint.Edge.AsyncMode) } + +// IsNewerThan225 returns true if the agent version is newer than 2.25.0 +// this is used to check if the agent is compatible with the new diagnostics feature +func IsNewerThan225(agentVersion string) bool { + v1, _ := version.NewVersion(agentVersion) + v2, _ := version.NewVersion("2.25.0") + return v1.GreaterThanOrEqual(v2) +} diff --git a/pkg/endpoints/utils_test.go b/pkg/endpoints/utils_test.go index 47ef2db8d..4231cf60a 100644 --- a/pkg/endpoints/utils_test.go +++ b/pkg/endpoints/utils_test.go @@ -202,3 +202,61 @@ func TestHasDirectConnectivity(t *testing.T) { }) } } + +func TestIsStandardEdgeEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint *portainer.Endpoint + expected bool + }{ + { + name: "StandardEdgeEndpoint", + endpoint: &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, + }, + expected: true, + }, + { + name: "AsyncEdgeEndpoint", + endpoint: &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsStandardEdgeEndpoint(tt.endpoint) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsNewerThan225(t *testing.T) { + tests := []struct { + name string + version string + expected bool + }{ + { + name: "NewerThan225", + version: "2.25.1", + expected: true, + }, + { + name: "OlderThan225", + version: "2.24.0", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsNewerThan225(tt.version) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/snapshot/docker_test.go b/pkg/snapshot/docker_test.go deleted file mode 100644 index 8df14bc39..000000000 --- a/pkg/snapshot/docker_test.go +++ /dev/null @@ -1 +0,0 @@ -package snapshot diff --git a/pkg/snapshot/kubernetes_test.go b/pkg/snapshot/kubernetes_test.go index 8df14bc39..142241a5c 100644 --- a/pkg/snapshot/kubernetes_test.go +++ b/pkg/snapshot/kubernetes_test.go @@ -1 +1,44 @@ package snapshot + +import ( + "context" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kfake "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateKubernetesSnapshot(t *testing.T) { + cli := kfake.NewSimpleClientset() + kubernetesSnapshot := &portainer.KubernetesSnapshot{} + + serverInfo, err := cli.Discovery().ServerVersion() + if err != nil { + t.Fatalf("error getting the kubernetesserver version: %v", err) + } + + kubernetesSnapshot.KubernetesVersion = serverInfo.GitVersion + require.Equal(t, kubernetesSnapshot.KubernetesVersion, serverInfo.GitVersion) + + nodeList, err := cli.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + t.Fatalf("error listing kubernetes nodes: %v", err) + } + + var totalCPUs, totalMemory int64 + for _, node := range nodeList.Items { + totalCPUs += node.Status.Capacity.Cpu().Value() + totalMemory += node.Status.Capacity.Memory().Value() + } + + kubernetesSnapshot.TotalCPU = totalCPUs + kubernetesSnapshot.TotalMemory = totalMemory + kubernetesSnapshot.NodeCount = len(nodeList.Items) + require.Equal(t, kubernetesSnapshot.TotalCPU, totalCPUs) + require.Equal(t, kubernetesSnapshot.TotalMemory, totalMemory) + require.Equal(t, kubernetesSnapshot.NodeCount, len(nodeList.Items)) + + t.Logf("Kubernetes snapshot: %+v", kubernetesSnapshot) +} From 03575186a7966ab3ca5014e24b5b3e7280d077bc Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Mon, 10 Feb 2025 10:46:36 +1300 Subject: [PATCH 015/212] remove deprecated api endpoints - develop [BE-11510] (#399) --- .../customtemplates/customtemplate_create.go | 25 ---- api/http/handler/customtemplates/handler.go | 2 - api/http/handler/edgejobs/edgejob_create.go | 23 ---- api/http/handler/edgejobs/handler.go | 3 - .../handler/edgestacks/edgestack_create.go | 23 ---- .../edgestacks/edgestack_status_delete.go | 87 ------------ .../edgestack_status_delete_test.go | 30 ----- api/http/handler/edgestacks/handler.go | 4 - .../edgetemplates/edgetemplate_list.go | 71 ---------- api/http/handler/edgetemplates/handler.go | 32 ----- api/http/handler/handler.go | 4 - api/http/handler/helm/handler.go | 6 - api/http/handler/helm/user_helm_repos.go | 127 ------------------ api/http/handler/stacks/handler.go | 3 - api/http/handler/stacks/stack_create.go | 51 ------- api/http/handler/system/handler.go | 4 - api/http/handler/system/nodes_count.go | 20 --- api/http/handler/system/version.go | 18 --- api/http/handler/templates/handler.go | 2 - .../handler/templates/template_file_old.go | 93 ------------- api/http/server.go | 5 - app/constants.ts | 1 - app/ng-constants.ts | 2 - 23 files changed, 636 deletions(-) delete mode 100644 api/http/handler/edgestacks/edgestack_status_delete.go delete mode 100644 api/http/handler/edgestacks/edgestack_status_delete_test.go delete mode 100644 api/http/handler/edgetemplates/edgetemplate_list.go delete mode 100644 api/http/handler/edgetemplates/handler.go delete mode 100644 api/http/handler/helm/user_helm_repos.go delete mode 100644 api/http/handler/templates/template_file_old.go diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index a5b6ecdee..0f9c8f1a3 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -482,28 +482,3 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po return customTemplate, nil } - -// @id CustomTemplateCreate -// @summary Create a custom template -// @description Create a custom template. -// @description **Access policy**: authenticated -// @tags custom_templates -// @security ApiKeyAuth -// @security jwt -// @accept json,multipart/form-data -// @produce json -// @param method query string true "method for creating template" Enums(string, file, repository) -// @param body body object true "for body documentation see the relevant /custom_templates/{method} endpoint" -// @success 200 {object} portainer.CustomTemplate -// @failure 400 "Invalid request" -// @failure 500 "Server error" -// @deprecated -// @router /custom_templates [post] -func deprecatedCustomTemplateCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { - method, err := request.RetrieveQueryParameter(r, "method", false) - if err != nil { - return "", httperror.BadRequest("Invalid query parameter: method", err) - } - - return "/custom_templates/create/" + method, nil -} diff --git a/api/http/handler/customtemplates/handler.go b/api/http/handler/customtemplates/handler.go index 1bb148af6..0da63d81f 100644 --- a/api/http/handler/customtemplates/handler.go +++ b/api/http/handler/customtemplates/handler.go @@ -7,7 +7,6 @@ import ( "github.com/gorilla/mux" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" ) @@ -33,7 +32,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor h.Handle("/custom_templates/create/{method}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost) - h.Handle("/custom_templates", middlewares.Deprecated(h, deprecatedCustomTemplateCreateUrlParser)).Methods(http.MethodPost) // Deprecated h.Handle("/custom_templates", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet) h.Handle("/custom_templates/{id}", diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index 252770aa2..0f7bff73d 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -271,26 +271,3 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo return tx.EdgeJob().CreateWithID(edgeJob.ID, edgeJob) } - -// @id EdgeJobCreate -// @summary Create an EdgeJob -// @description **Access policy**: administrator -// @tags edge_jobs -// @security ApiKeyAuth -// @security jwt -// @produce json -// @param method query string true "Creation Method" Enums(file, string) -// @param body body object true "for body documentation see the relevant /edge_jobs/create/{method} endpoint" -// @success 200 {object} portainer.EdgeGroup -// @failure 503 "Edge compute features are disabled" -// @failure 500 -// @deprecated -// @router /edge_jobs [post] -func deprecatedEdgeJobCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { - method, err := request.RetrieveQueryParameter(r, "method", false) - if err != nil { - return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) - } - - return "/edge_jobs/create/" + method, nil -} diff --git a/api/http/handler/edgejobs/handler.go b/api/http/handler/edgejobs/handler.go index 93f210bb5..ab3d66b3b 100644 --- a/api/http/handler/edgejobs/handler.go +++ b/api/http/handler/edgejobs/handler.go @@ -6,7 +6,6 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/response" @@ -30,8 +29,6 @@ func NewHandler(bouncer security.BouncerService) *Handler { h.Handle("/edge_jobs", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet) - h.Handle("/edge_jobs", - bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeJobCreateUrlParser)))).Methods(http.MethodPost) h.Handle("/edge_jobs/create/{method}", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost) h.Handle("/edge_jobs/{id}", diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 12dfe620a..65e77764a 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -55,26 +55,3 @@ func (handler *Handler) createSwarmStack(tx dataservices.DataStoreTx, method str return nil, httperrors.NewInvalidPayloadError("Invalid value for query parameter: method. Value must be one of: string, repository or file") } - -// @id EdgeStackCreate -// @summary Create an EdgeStack -// @description **Access policy**: administrator -// @tags edge_stacks -// @security ApiKeyAuth -// @security jwt -// @produce json -// @param method query string true "Creation Method" Enums(file,string,repository) -// @param body body object true "for body documentation see the relevant /edge_stacks/create/{method} endpoint" -// @success 200 {object} portainer.EdgeStack -// @failure 500 -// @failure 503 "Edge compute features are disabled" -// @deprecated -// @router /edge_stacks [post] -func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { - method, err := request.RetrieveQueryParameter(r, "method", false) - if err != nil { - return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) - } - - return "/edge_stacks/create/" + method, nil -} diff --git a/api/http/handler/edgestacks/edgestack_status_delete.go b/api/http/handler/edgestacks/edgestack_status_delete.go deleted file mode 100644 index eb54951b6..000000000 --- a/api/http/handler/edgestacks/edgestack_status_delete.go +++ /dev/null @@ -1,87 +0,0 @@ -package edgestacks - -import ( - "errors" - "net/http" - "time" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/http/middlewares" - httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/request" - "github.com/portainer/portainer/pkg/libhttp/response" -) - -// @id EdgeStackStatusDelete -// @summary Delete an EdgeStack status -// @description Authorized only if the request is done by an Edge Environment(Endpoint) -// @tags edge_stacks -// @produce json -// @param id path int true "EdgeStack Id" -// @param environmentId path int true "Environment identifier" -// @success 200 {object} portainer.EdgeStack -// @failure 500 -// @failure 400 -// @failure 404 -// @failure 403 -// @deprecated -// @router /edge_stacks/{id}/status/{environmentId} [delete] -func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return httperror.BadRequest("Invalid stack identifier route variable", err) - } - - endpoint, err := middlewares.FetchEndpoint(r) - if err != nil { - return httperror.InternalServerError("Unable to retrieve a valid endpoint from the handler context", err) - } - - err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) - if err != nil { - return httperror.Forbidden("Permission denied to access environment", err) - } - - var stack *portainer.EdgeStack - err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { - stack, err = handler.deleteEdgeStackStatus(tx, portainer.EdgeStackID(stackID), endpoint) - return err - }) - if err != nil { - var httpErr *httperror.HandlerError - if errors.As(err, &httpErr) { - return httpErr - } - - return httperror.InternalServerError("Unexpected error", err) - } - - return response.JSON(w, stack) -} - -func (handler *Handler) deleteEdgeStackStatus(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID, endpoint *portainer.Endpoint) (*portainer.EdgeStack, error) { - stack, err := tx.EdgeStack().EdgeStack(stackID) - if err != nil { - return nil, handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database") - } - - environmentStatus, ok := stack.Status[endpoint.ID] - if !ok { - environmentStatus = portainer.EdgeStackStatus{} - } - - environmentStatus.Status = append(environmentStatus.Status, portainer.EdgeStackDeploymentStatus{ - Time: time.Now().Unix(), - Type: portainer.EdgeStackStatusRemoved, - }) - - stack.Status[endpoint.ID] = environmentStatus - - err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack) - if err != nil { - return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err) - } - - return stack, nil -} diff --git a/api/http/handler/edgestacks/edgestack_status_delete_test.go b/api/http/handler/edgestacks/edgestack_status_delete_test.go deleted file mode 100644 index 7a3db1315..000000000 --- a/api/http/handler/edgestacks/edgestack_status_delete_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package edgestacks - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - portainer "github.com/portainer/portainer/api" -) - -func TestDeleteStatus(t *testing.T) { - handler, _ := setupHandler(t) - - endpoint := createEndpoint(t, handler.DataStore) - edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID) - - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil) - if err != nil { - t.Fatal("request error:", err) - } - - req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code) - } -} diff --git a/api/http/handler/edgestacks/handler.go b/api/http/handler/edgestacks/handler.go index 290524cb0..9fa90776f 100644 --- a/api/http/handler/edgestacks/handler.go +++ b/api/http/handler/edgestacks/handler.go @@ -37,8 +37,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor h.Handle("/edge_stacks/create/{method}", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost) - h.Handle("/edge_stacks", - bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeStackCreateUrlParser)))).Methods(http.MethodPost) // Deprecated h.Handle("/edge_stacks", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet) h.Handle("/edge_stacks/{id}", @@ -55,8 +53,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor edgeStackStatusRouter := h.NewRoute().Subrouter() edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id")) - edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete) - return h } diff --git a/api/http/handler/edgetemplates/edgetemplate_list.go b/api/http/handler/edgetemplates/edgetemplate_list.go deleted file mode 100644 index 5b2c7254d..000000000 --- a/api/http/handler/edgetemplates/edgetemplate_list.go +++ /dev/null @@ -1,71 +0,0 @@ -package edgetemplates - -import ( - "net/http" - "slices" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/client" - httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/response" - - "github.com/segmentio/encoding/json" -) - -type templateFileFormat struct { - Version string `json:"version"` - Templates []portainer.Template `json:"templates"` -} - -// @id EdgeTemplateList -// @deprecated -// @summary Fetches the list of Edge Templates -// @description **Access policy**: administrator -// @tags edge_templates -// @security ApiKeyAuth -// @security jwt -// @accept json -// @produce json -// @success 200 {array} portainer.Template -// @failure 500 -// @router /edge_templates [get] -func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return httperror.InternalServerError("Unable to retrieve settings from the database", err) - } - - url := portainer.DefaultTemplatesURL - if settings.TemplatesURL != "" { - url = settings.TemplatesURL - } - - var templateData []byte - templateData, err = client.Get(url, 10) - if err != nil { - return httperror.InternalServerError("Unable to retrieve external templates", err) - } - - var templateFile templateFileFormat - - err = json.Unmarshal(templateData, &templateFile) - if err != nil { - return httperror.InternalServerError("Unable to parse template file", err) - } - - // We only support version 3 of the template format - // this is only a temporary fix until we have custom edge templates - if templateFile.Version != "3" { - return httperror.InternalServerError("Unsupported template version", nil) - } - - filteredTemplates := make([]portainer.Template, 0) - - for _, template := range templateFile.Templates { - if slices.Contains(template.Categories, "edge") && slices.Contains([]portainer.TemplateType{portainer.ComposeStackTemplate, portainer.SwarmStackTemplate}, template.Type) { - filteredTemplates = append(filteredTemplates, template) - } - } - - return response.JSON(w, filteredTemplates) -} diff --git a/api/http/handler/edgetemplates/handler.go b/api/http/handler/edgetemplates/handler.go deleted file mode 100644 index d6c98553f..000000000 --- a/api/http/handler/edgetemplates/handler.go +++ /dev/null @@ -1,32 +0,0 @@ -package edgetemplates - -import ( - "net/http" - - "github.com/portainer/portainer/api/dataservices" - "github.com/portainer/portainer/api/http/middlewares" - "github.com/portainer/portainer/api/http/security" - httperror "github.com/portainer/portainer/pkg/libhttp/error" - - "github.com/gorilla/mux" -) - -// Handler is the HTTP handler used to handle edge environment(endpoint) operations. -type Handler struct { - *mux.Router - requestBouncer security.BouncerService - DataStore dataservices.DataStore -} - -// NewHandler creates a handler to manage environment(endpoint) operations. -func NewHandler(bouncer security.BouncerService) *Handler { - h := &Handler{ - Router: mux.NewRouter(), - requestBouncer: bouncer, - } - - h.Handle("/edge_templates", - bouncer.AdminAccess(middlewares.Deprecated(httperror.LoggerHandler(h.edgeTemplateList), func(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { return "", nil }))).Methods(http.MethodGet) - - return h -} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 4d7973170..4325d9f91 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -11,7 +11,6 @@ import ( "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" "github.com/portainer/portainer/api/http/handler/edgestacks" - "github.com/portainer/portainer/api/http/handler/edgetemplates" "github.com/portainer/portainer/api/http/handler/endpointedge" "github.com/portainer/portainer/api/http/handler/endpointgroups" "github.com/portainer/portainer/api/http/handler/endpointproxy" @@ -50,7 +49,6 @@ type Handler struct { EdgeGroupsHandler *edgegroups.Handler EdgeJobsHandler *edgejobs.Handler EdgeStacksHandler *edgestacks.Handler - EdgeTemplatesHandler *edgetemplates.Handler EndpointEdgeHandler *endpointedge.Handler EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler @@ -190,8 +188,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/edge_jobs"): http.StripPrefix("/api", h.EdgeJobsHandler).ServeHTTP(w, r) - case strings.HasPrefix(r.URL.Path, "/api/edge_templates"): - http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"): http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/kubernetes"): diff --git a/api/http/handler/helm/handler.go b/api/http/handler/helm/handler.go index 0108f7ef1..fb941940b 100644 --- a/api/http/handler/helm/handler.go +++ b/api/http/handler/helm/handler.go @@ -53,12 +53,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor h.Handle("/{id}/kubernetes/helm", httperror.LoggerHandler(h.helmInstall)).Methods(http.MethodPost) - // Deprecated - h.Handle("/{id}/kubernetes/helm/repositories", - httperror.LoggerHandler(h.userGetHelmRepos)).Methods(http.MethodGet) - h.Handle("/{id}/kubernetes/helm/repositories", - httperror.LoggerHandler(h.userCreateHelmRepo)).Methods(http.MethodPost) - return h } diff --git a/api/http/handler/helm/user_helm_repos.go b/api/http/handler/helm/user_helm_repos.go deleted file mode 100644 index 5197f88f9..000000000 --- a/api/http/handler/helm/user_helm_repos.go +++ /dev/null @@ -1,127 +0,0 @@ -package helm - -import ( - "net/http" - "strings" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/security" - "github.com/portainer/portainer/pkg/libhelm" - httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/request" - "github.com/portainer/portainer/pkg/libhttp/response" - - "github.com/pkg/errors" -) - -type helmUserRepositoryResponse struct { - GlobalRepository string `json:"GlobalRepository"` - UserRepositories []portainer.HelmUserRepository `json:"UserRepositories"` -} - -type addHelmRepoUrlPayload struct { - URL string `json:"url"` -} - -func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error { - return libhelm.ValidateHelmRepositoryURL(p.URL, nil) -} - -// @id HelmUserRepositoryCreateDeprecated -// @summary Create a user helm repository -// @description Create a user helm repository. -// @description **Access policy**: authenticated -// @tags helm -// @security ApiKeyAuth -// @security jwt -// @accept json -// @produce json -// @param id path int true "Environment(Endpoint) identifier" -// @param payload body addHelmRepoUrlPayload true "Helm Repository" -// @success 200 {object} portainer.HelmUserRepository "Success" -// @failure 400 "Invalid request" -// @failure 403 "Permission denied" -// @failure 500 "Server error" -// @deprecated -// @router /endpoints/{id}/kubernetes/helm/repositories [post] -func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - tokenData, err := security.RetrieveTokenData(r) - if err != nil { - return httperror.InternalServerError("Unable to retrieve user authentication token", err) - } - userID := tokenData.ID - - p := new(addHelmRepoUrlPayload) - err = request.DecodeAndValidateJSONPayload(r, p) - if err != nil { - return httperror.BadRequest("Invalid Helm repository URL", err) - } - - // lowercase, remove trailing slash - p.URL = strings.TrimSuffix(strings.ToLower(p.URL), "/") - - records, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID) - if err != nil { - return httperror.InternalServerError("Unable to access the DataStore", err) - } - - // check if repo already exists - by doing case insensitive comparison - for _, record := range records { - if strings.EqualFold(record.URL, p.URL) { - errMsg := "Helm repo already registered for user" - return httperror.BadRequest(errMsg, errors.New(errMsg)) - } - } - - record := portainer.HelmUserRepository{ - UserID: userID, - URL: p.URL, - } - - err = handler.dataStore.HelmUserRepository().Create(&record) - if err != nil { - return httperror.InternalServerError("Unable to save a user Helm repository URL", err) - } - - return response.JSON(w, record) -} - -// @id HelmUserRepositoriesListDeprecated -// @summary List a users helm repositories -// @description Inspect a user helm repositories. -// @description **Access policy**: authenticated -// @tags helm -// @security ApiKeyAuth -// @security jwt -// @produce json -// @param id path int true "User identifier" -// @success 200 {object} helmUserRepositoryResponse "Success" -// @failure 400 "Invalid request" -// @failure 403 "Permission denied" -// @failure 500 "Server error" -// @deprecated -// @router /endpoints/{id}/kubernetes/helm/repositories [get] -func (handler *Handler) userGetHelmRepos(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - tokenData, err := security.RetrieveTokenData(r) - if err != nil { - return httperror.InternalServerError("Unable to retrieve user authentication token", err) - } - userID := tokenData.ID - - settings, err := handler.dataStore.Settings().Settings() - if err != nil { - return httperror.InternalServerError("Unable to retrieve settings from the database", err) - } - - userRepos, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID) - if err != nil { - return httperror.InternalServerError("Unable to get user Helm repositories", err) - } - - resp := helmUserRepositoryResponse{ - GlobalRepository: settings.HelmRepositoryURL, - UserRepositories: userRepos, - } - - return response.JSON(w, resp) -} diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 7e5cce040..2ad40a182 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -11,7 +11,6 @@ import ( "github.com/portainer/portainer/api/dataservices" dockerclient "github.com/portainer/portainer/api/docker/client" "github.com/portainer/portainer/api/docker/consts" - "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/endpointutils" @@ -62,8 +61,6 @@ func NewHandler(bouncer security.BouncerService) *Handler { h.Handle("/stacks/create/{type}/{method}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost) - h.Handle("/stacks", - bouncer.AuthenticatedAccess(middlewares.Deprecated(h, deprecatedStackCreateUrlParser))).Methods(http.MethodPost) // Deprecated h.Handle("/stacks", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet) h.Handle("/stacks/{id}", diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index f45592d09..cb297f9d7 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -1,7 +1,6 @@ package stacks import ( - "fmt" "net/http" portainer "github.com/portainer/portainer/api" @@ -141,53 +140,3 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port return response.JSON(w, stack) } - -func getStackTypeFromQueryParameter(r *http.Request) (string, error) { - stackType, err := request.RetrieveNumericQueryParameter(r, "type", false) - if err != nil { - return "", err - } - - switch stackType { - case 1: - return "swarm", nil - case 2: - return "standalone", nil - case 3: - return "kubernetes", nil - } - - return "", errors.New(request.ErrInvalidQueryParameter) -} - -// @id StackCreate -// @summary Deploy a new stack -// @description Deploy a new stack into a Docker environment(endpoint) specified via the environment(endpoint) identifier. -// @description **Access policy**: authenticated -// @tags stacks -// @security ApiKeyAuth -// @security jwt -// @accept json,multipart/form-data -// @produce json -// @param type query int true "Stack deployment type. Possible values: 1 (Swarm stack), 2 (Compose stack) or 3 (Kubernetes stack)." Enums(1,2,3) -// @param method query string true "Stack deployment method. Possible values: file, string, repository or url." Enums(string, file, repository, url) -// @param endpointId query int true "Identifier of the environment(endpoint) that will be used to deploy the stack" -// @param body body object true "for body documentation see the relevant /stacks/create/{type}/{method} endpoint" -// @success 200 {object} portainer.Stack -// @failure 400 "Invalid request" -// @failure 500 "Server error" -// @deprecated -// @router /stacks [post] -func deprecatedStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { - method, err := request.RetrieveQueryParameter(r, "method", false) - if err != nil { - return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) - } - - stackType, err := getStackTypeFromQueryParameter(r) - if err != nil { - return "", httperror.BadRequest("Invalid query parameter: type", err) - } - - return fmt.Sprintf("/stacks/create/%s/%s", stackType, method), nil -} diff --git a/api/http/handler/system/handler.go b/api/http/handler/system/handler.go index d2e3f5485..4cab43332 100644 --- a/api/http/handler/system/handler.go +++ b/api/http/handler/system/handler.go @@ -59,10 +59,6 @@ func NewHandler(bouncer security.BouncerService, // Deprecated /status endpoint, will be removed in the future. h.Handle("/status", bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspectDeprecated))).Methods(http.MethodGet) - h.Handle("/status/version", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.versionDeprecated))).Methods(http.MethodGet) - h.Handle("/status/nodes", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.statusNodesCountDeprecated))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/system/nodes_count.go b/api/http/handler/system/nodes_count.go index 0b9971619..94ab320fa 100644 --- a/api/http/handler/system/nodes_count.go +++ b/api/http/handler/system/nodes_count.go @@ -8,8 +8,6 @@ import ( "github.com/portainer/portainer/api/internal/snapshot" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/response" - - "github.com/rs/zerolog/log" ) type nodesCountResponse struct { @@ -44,21 +42,3 @@ func (handler *Handler) systemNodesCount(w http.ResponseWriter, r *http.Request) return response.JSON(w, &nodesCountResponse{Nodes: nodes}) } - -// @id statusNodesCount -// @summary Retrieve the count of nodes -// @deprecated -// @description Deprecated: use the `/system/nodes` endpoint instead. -// @description **Access policy**: authenticated -// @security ApiKeyAuth -// @security jwt -// @tags status -// @produce json -// @success 200 {object} nodesCountResponse "Success" -// @failure 500 "Server error" -// @router /status/nodes [get] -func (handler *Handler) statusNodesCountDeprecated(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - log.Warn().Msg("The /status/nodes endpoint is deprecated, please use the /system/nodes endpoint instead") - - return handler.systemNodesCount(w, r) -} diff --git a/api/http/handler/system/version.go b/api/http/handler/system/version.go index 50ad2f6af..9d80b88a9 100644 --- a/api/http/handler/system/version.go +++ b/api/http/handler/system/version.go @@ -106,21 +106,3 @@ func HasNewerVersion(currentVersion, latestVersion string) bool { return currentVersionSemver.LessThan(*latestVersionSemver) } - -// @id Version -// @summary Check for portainer updates -// @deprecated -// @description Deprecated: use the `/system/version` endpoint instead. -// @description Check if portainer has an update available -// @description **Access policy**: authenticated -// @security ApiKeyAuth -// @security jwt -// @tags status -// @produce json -// @success 200 {object} versionResponse "Success" -// @router /status/version [get] -func (handler *Handler) versionDeprecated(w http.ResponseWriter, r *http.Request) { - log.Warn().Msg("The /status/version endpoint is deprecated, please use the /system/version endpoint instead") - - handler.version(w, r) -} diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index e604e8abe..1e7b7f05d 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -29,7 +29,5 @@ func NewHandler(bouncer security.BouncerService) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) h.Handle("/templates/{id}/file", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateFile))).Methods(http.MethodPost) - h.Handle("/templates/file", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.templateFileOld))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/templates/template_file_old.go b/api/http/handler/templates/template_file_old.go deleted file mode 100644 index 91ac038c5..000000000 --- a/api/http/handler/templates/template_file_old.go +++ /dev/null @@ -1,93 +0,0 @@ -package templates - -import ( - "errors" - "net/http" - - httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/request" - "github.com/portainer/portainer/pkg/libhttp/response" - "github.com/rs/zerolog/log" -) - -type filePayload struct { - // URL of a git repository where the file is stored - RepositoryURL string `example:"https://github.com/portainer/portainer-compose" validate:"required"` - // Path to the file inside the git repository - ComposeFilePathInRepository string `example:"./subfolder/docker-compose.yml" validate:"required"` -} - -func (payload *filePayload) Validate(r *http.Request) error { - if len(payload.RepositoryURL) == 0 { - return errors.New("Invalid repository url") - } - - if len(payload.ComposeFilePathInRepository) == 0 { - return errors.New("Invalid file path") - } - - return nil -} - -func (handler *Handler) ifRequestedTemplateExists(payload *filePayload) *httperror.HandlerError { - response, httpErr := handler.fetchTemplates() - if httpErr != nil { - return httpErr - } - - for _, t := range response.Templates { - if t.Repository.URL == payload.RepositoryURL && t.Repository.StackFile == payload.ComposeFilePathInRepository { - return nil - } - } - return httperror.InternalServerError("Invalid template", errors.New("requested template does not exist")) -} - -// @id TemplateFileOld -// @summary Get a template's file -// @deprecated -// @description Get a template's file -// @description **Access policy**: authenticated -// @tags templates -// @security ApiKeyAuth -// @security jwt -// @accept json -// @produce json -// @param body body filePayload true "File details" -// @success 200 {object} fileResponse "Success" -// @failure 400 "Invalid request" -// @failure 500 "Server error" -// @router /templates/file [post] -func (handler *Handler) templateFileOld(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - log.Warn().Msg("This api is deprecated. Please use /templates/{id}/file instead") - - var payload filePayload - err := request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return httperror.BadRequest("Invalid request payload", err) - } - - if err := handler.ifRequestedTemplateExists(&payload); err != nil { - return err - } - - projectPath, err := handler.FileService.GetTemporaryPath() - if err != nil { - return httperror.InternalServerError("Unable to create temporary folder", err) - } - - defer handler.cleanUp(projectPath) - - err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "", false) - if err != nil { - return httperror.InternalServerError("Unable to clone git repository", err) - } - - fileContent, err := handler.FileService.GetFileContent(projectPath, payload.ComposeFilePathInRepository) - if err != nil { - return httperror.InternalServerError("Failed loading file content", err) - } - - return response.JSON(w, fileResponse{FileContent: string(fileContent)}) - -} diff --git a/api/http/server.go b/api/http/server.go index ec86e6220..c5bb0d40f 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -24,7 +24,6 @@ import ( "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" "github.com/portainer/portainer/api/http/handler/edgestacks" - "github.com/portainer/portainer/api/http/handler/edgetemplates" "github.com/portainer/portainer/api/http/handler/endpointedge" "github.com/portainer/portainer/api/http/handler/endpointgroups" "github.com/portainer/portainer/api/http/handler/endpointproxy" @@ -169,9 +168,6 @@ func (server *Server) Start() error { edgeStacksHandler.GitService = server.GitService edgeStacksHandler.KubernetesDeployer = server.KubernetesDeployer - var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer) - edgeTemplatesHandler.DataStore = server.DataStore - var endpointHandler = endpoints.NewHandler(requestBouncer) endpointHandler.DataStore = server.DataStore endpointHandler.FileService = server.FileService @@ -306,7 +302,6 @@ func (server *Server) Start() error { EdgeGroupsHandler: edgeGroupsHandler, EdgeJobsHandler: edgeJobsHandler, EdgeStacksHandler: edgeStacksHandler, - EdgeTemplatesHandler: edgeTemplatesHandler, EndpointGroupHandler: endpointGroupHandler, EndpointHandler: endpointHandler, EndpointHelmHandler: endpointHelmHandler, diff --git a/app/constants.ts b/app/constants.ts index 8a8e687d2..e61172d27 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -5,7 +5,6 @@ export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates'; export const API_ENDPOINT_EDGE_GROUPS = 'api/edge_groups'; export const API_ENDPOINT_EDGE_JOBS = 'api/edge_jobs'; export const API_ENDPOINT_EDGE_STACKS = 'api/edge_stacks'; -export const API_ENDPOINT_EDGE_TEMPLATES = 'api/edge_templates'; export const API_ENDPOINT_ENDPOINTS = 'api/endpoints'; export const API_ENDPOINT_ENDPOINT_GROUPS = 'api/endpoint_groups'; export const API_ENDPOINT_KUBERNETES = 'api/kubernetes'; diff --git a/app/ng-constants.ts b/app/ng-constants.ts index 527d42132..87fa45b97 100644 --- a/app/ng-constants.ts +++ b/app/ng-constants.ts @@ -7,7 +7,6 @@ import { API_ENDPOINT_EDGE_GROUPS, API_ENDPOINT_EDGE_JOBS, API_ENDPOINT_EDGE_STACKS, - API_ENDPOINT_EDGE_TEMPLATES, API_ENDPOINT_ENDPOINTS, API_ENDPOINT_ENDPOINT_GROUPS, API_ENDPOINT_KUBERNETES, @@ -42,7 +41,6 @@ export const constantsModule = angular .constant('API_ENDPOINT_EDGE_GROUPS', API_ENDPOINT_EDGE_GROUPS) .constant('API_ENDPOINT_EDGE_JOBS', API_ENDPOINT_EDGE_JOBS) .constant('API_ENDPOINT_EDGE_STACKS', API_ENDPOINT_EDGE_STACKS) - .constant('API_ENDPOINT_EDGE_TEMPLATES', API_ENDPOINT_EDGE_TEMPLATES) .constant('API_ENDPOINT_ENDPOINTS', API_ENDPOINT_ENDPOINTS) .constant('API_ENDPOINT_ENDPOINT_GROUPS', API_ENDPOINT_ENDPOINT_GROUPS) .constant('API_ENDPOINT_KUBERNETES', API_ENDPOINT_KUBERNETES) From 4bb80d3e3afde96e257025e92cea33754c4b05c5 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:05:15 +1300 Subject: [PATCH 016/212] fix(setting): failed to persist edge computer setting [BE-11403] (#395) --- api/cmd/portainer/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index edc9cb897..88e21f17b 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -238,10 +238,10 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer. return err } - settings.SnapshotInterval = *cmp.Or(flags.SnapshotInterval, &settings.SnapshotInterval) - settings.LogoURL = *cmp.Or(flags.Logo, &settings.LogoURL) - settings.EnableEdgeComputeFeatures = *cmp.Or(flags.EnableEdgeComputeFeatures, &settings.EnableEdgeComputeFeatures) - settings.TemplatesURL = *cmp.Or(flags.Templates, &settings.TemplatesURL) + settings.SnapshotInterval = cmp.Or(*flags.SnapshotInterval, settings.SnapshotInterval) + settings.LogoURL = cmp.Or(*flags.Logo, settings.LogoURL) + settings.EnableEdgeComputeFeatures = cmp.Or(*flags.EnableEdgeComputeFeatures, settings.EnableEdgeComputeFeatures) + settings.TemplatesURL = cmp.Or(*flags.Templates, settings.TemplatesURL) if *flags.Labels != nil { settings.BlackListedLabels = *flags.Labels From b25bf1e341e2f500d873c5ca23824b6789cd4d2d Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:08:27 +1300 Subject: [PATCH 017/212] fix(podman): missing filter in homepage [BE-11502] (#404) --- .../EnvironmentList/EnvironmentList.tsx | 1 + .../EnvironmentListFilters.tsx | 15 ++++++++-- .../environments/environment.service/index.ts | 2 ++ .../queries/useEnvironmentList.ts | 29 ++++++++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx index 4805ce6e6..9884048b3 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx @@ -109,6 +109,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { agentVersions, updateInformation: isBE, edgeAsync: getEdgeAsyncValue(connectionTypes), + platformTypes, }; const queryWithSort = { diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx index 3c4d993e4..06d1c5108 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx @@ -209,6 +209,7 @@ function getPlatformTypeOptions(connectionTypes: ConnectionType[]) { { value: PlatformType.Docker, label: 'Docker' }, { value: PlatformType.Azure, label: 'Azure' }, { value: PlatformType.Kubernetes, label: 'Kubernetes' }, + { value: PlatformType.Podman, label: 'Podman' }, ]; if (connectionTypes.length === 0) { @@ -216,15 +217,25 @@ function getPlatformTypeOptions(connectionTypes: ConnectionType[]) { } const connectionTypePlatformType = { - [ConnectionType.API]: [PlatformType.Docker, PlatformType.Azure], - [ConnectionType.Agent]: [PlatformType.Docker, PlatformType.Kubernetes], + [ConnectionType.API]: [ + PlatformType.Docker, + PlatformType.Azure, + PlatformType.Podman, + ], + [ConnectionType.Agent]: [ + PlatformType.Docker, + PlatformType.Kubernetes, + PlatformType.Podman, + ], [ConnectionType.EdgeAgentStandard]: [ PlatformType.Kubernetes, PlatformType.Docker, + PlatformType.Podman, ], [ConnectionType.EdgeAgentAsync]: [ PlatformType.Docker, PlatformType.Kubernetes, + PlatformType.Podman, ], }; diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts index 09844e6ce..8971a4af0 100644 --- a/app/react/portainer/environments/environment.service/index.ts +++ b/app/react/portainer/environments/environment.service/index.ts @@ -6,6 +6,7 @@ import { EnvironmentSecuritySettings, EnvironmentStatus, EnvironmentGroupId, + PlatformType, } from '@/react/portainer/environments/types'; import { type TagId } from '@/portainer/tags/types'; import { UserId } from '@/portainer/users/types'; @@ -45,6 +46,7 @@ export interface BaseEnvironmentsQueryParams { agentVersions?: string[]; updateInformation?: boolean; edgeCheckInPassedSeconds?: number; + platformTypes?: PlatformType[]; } export type EnvironmentsQueryParams = BaseEnvironmentsQueryParams & diff --git a/app/react/portainer/environments/queries/useEnvironmentList.ts b/app/react/portainer/environments/queries/useEnvironmentList.ts index ef545b06f..850fc7cfd 100644 --- a/app/react/portainer/environments/queries/useEnvironmentList.ts +++ b/app/react/portainer/environments/queries/useEnvironmentList.ts @@ -1,8 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import { withError } from '@/react-tools/react-query'; +import { + PlatformType, + EnvironmentStatus, +} from '@/react/portainer/environments/types'; -import { EnvironmentStatus } from '../types'; import { EnvironmentsQueryParams, getEnvironments, @@ -98,6 +101,30 @@ export function useEnvironmentList( } ); + if (data?.value && query && query.platformTypes) { + const platforms = Array.from(query.platformTypes); + + if ( + platforms.includes(PlatformType.Podman) !== + platforms.includes(PlatformType.Docker) + ) { + const isPodmanSelected = platforms.includes(PlatformType.Podman); + const containerEngineToExclude = isPodmanSelected ? 'docker' : 'podman'; + + const filteredList = data?.value.filter( + (env) => env.ContainerEngine !== containerEngineToExclude + ); + + return { + isLoading, + environments: filteredList, + totalCount: data ? data.totalCount : 0, + totalAvailable: data ? data.totalAvailable : 0, + updateAvailable: data ? data.updateAvailable : false, + }; + } + } + return { isLoading, environments: data ? data.value : [], From 2d3e5c3499812d359476b8440bdc5ffb5255a4ae Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Tue, 11 Feb 2025 15:36:29 +1300 Subject: [PATCH 018/212] workaround: leave the globally set helm repo to empty and add disclaimer - develop (#409) --- api/database/boltdb/json_test.go | 2 +- .../test_data/output_24_to_latest.json | 2 +- api/http/handler/helm/helm_install_test.go | 2 +- .../handler/helm/helm_repo_search_test.go | 2 +- api/http/handler/helm/helm_show_test.go | 2 +- api/http/handler/settings/settings_update.go | 2 +- api/portainer.go | 10 ++--- .../helm-templates-list.html | 38 +++++++++++++++++-- .../KubeSettingsPanel/HelmSection.tsx | 24 +++++++++++- pkg/libhelm/binary/install_test.go | 12 +++--- pkg/libhelm/binary/search_repo_test.go | 2 +- pkg/libhelm/validate_repo_test.go | 2 +- 12 files changed, 76 insertions(+), 24 deletions(-) diff --git a/api/database/boltdb/json_test.go b/api/database/boltdb/json_test.go index 577aa2cfd..ba0863efd 100644 --- a/api/database/boltdb/json_test.go +++ b/api/database/boltdb/json_test.go @@ -10,7 +10,7 @@ import ( ) const ( - jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}` + jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://kubernetes.github.io/ingress-nginx","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}` passphrase = "my secret key" ) diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 43d872914..d9b9bf036 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -605,7 +605,7 @@ "GlobalDeploymentOptions": { "hideStacksFunctionality": false }, - "HelmRepositoryURL": "https://charts.bitnami.com/bitnami", + "HelmRepositoryURL": "", "InternalAuthSettings": { "RequiredPasswordLength": 12 }, diff --git a/api/http/handler/helm/helm_install_test.go b/api/http/handler/helm/helm_install_test.go index 869b80554..0b87d3a23 100644 --- a/api/http/handler/helm/helm_install_test.go +++ b/api/http/handler/helm/helm_install_test.go @@ -45,7 +45,7 @@ func Test_helmInstall(t *testing.T) { is.NotNil(h, "Handler should not fail") // Install a single chart. We expect to get these values back - options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://charts.bitnami.com/bitnami"} + options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://kubernetes.github.io/ingress-nginx"} optdata, err := json.Marshal(options) is.NoError(err) diff --git a/api/http/handler/helm/helm_repo_search_test.go b/api/http/handler/helm/helm_repo_search_test.go index 5fde5e642..a556908b9 100644 --- a/api/http/handler/helm/helm_repo_search_test.go +++ b/api/http/handler/helm/helm_repo_search_test.go @@ -20,7 +20,7 @@ func Test_helmRepoSearch(t *testing.T) { assert.NotNil(t, h, "Handler should not fail") - repos := []string{"https://charts.bitnami.com/bitnami", "https://portainer.github.io/k8s"} + repos := []string{"https://kubernetes.github.io/ingress-nginx", "https://portainer.github.io/k8s"} for _, repo := range repos { t.Run(repo, func(t *testing.T) { diff --git a/api/http/handler/helm/helm_show_test.go b/api/http/handler/helm/helm_show_test.go index dd3957c99..fed59388d 100644 --- a/api/http/handler/helm/helm_show_test.go +++ b/api/http/handler/helm/helm_show_test.go @@ -31,7 +31,7 @@ func Test_helmShow(t *testing.T) { t.Run(cmd, func(t *testing.T) { is.NotNil(h, "Handler should not fail") - repoUrlEncoded := url.QueryEscape("https://charts.bitnami.com/bitnami") + repoUrlEncoded := url.QueryEscape("https://kubernetes.github.io/ingress-nginx") chart := "nginx" req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s?repo=%s&chart=%s", cmd, repoUrlEncoded, chart), nil) rr := httptest.NewRecorder() diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 0b36dbc62..895da489c 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -46,7 +46,7 @@ type settingsUpdatePayload struct { // Whether telemetry is enabled EnableTelemetry *bool `example:"false"` // Helm repository URL - HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"` + HelmRepositoryURL *string `example:"https://kubernetes.github.io/ingress-nginx"` // Kubectl Shell Image KubectlShellImage *string `example:"portainer/kubectl-shell:latest"` // TrustOnFirstConnect makes Portainer accepting edge agent connection by default diff --git a/api/portainer.go b/api/portainer.go index 64ac47012..9a6d719e8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -588,7 +588,7 @@ type ( // User identifier UserID UserID `json:"UserId" example:"1"` // Helm repository URL - URL string `json:"URL" example:"https://charts.bitnami.com/bitnami"` + URL string `json:"URL" example:"https://kubernetes.github.io/ingress-nginx"` } // QuayRegistryData represents data required for Quay registry to work @@ -984,8 +984,8 @@ type ( KubeconfigExpiry string `json:"KubeconfigExpiry" example:"24h"` // Whether telemetry is enabled EnableTelemetry bool `json:"EnableTelemetry" example:"false"` - // Helm repository URL, defaults to "https://charts.bitnami.com/bitnami" - HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"` + // Helm repository URL, defaults to "" + HelmRepositoryURL string `json:"HelmRepositoryURL"` // KubectlImage, defaults to portainer/kubectl-shell KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"` // TrustOnFirstConnect makes Portainer accepting edge agent connection by default @@ -1672,8 +1672,8 @@ const ( DefaultEdgeAgentCheckinIntervalInSeconds = 5 // DefaultTemplatesURL represents the URL to the official templates supported by Portainer DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/v3/templates.json" - // DefaultHelmrepositoryURL represents the URL to the official templates supported by Bitnami - DefaultHelmRepositoryURL = "https://charts.bitnami.com/bitnami" + // DefaultHelmrepositoryURL set to empty string until oci support is added + DefaultHelmRepositoryURL = "" // DefaultUserSessionTimeout represents the default timeout after which the user session is cleared DefaultUserSessionTimeout = "8h" // DefaultUserSessionTimeout represents the default timeout after which the user session is cleared diff --git a/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.html b/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.html index 8f3f2fb4a..504c53af3 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.html +++ b/app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.html @@ -31,10 +31,40 @@ >Select the Helm chart to use. Bring further Helm charts into your selection list via User settings - Helm repositories.
- +
+
Select the Helm chart to use. Bring further Helm charts into your selection list via + User settings - Helm repositories.
+
+
+ + + + + +
+
+

Disclaimer

+
+ At present Portainer does not support OCI format Helm charts. Support for OCI charts will be available in a future release.
+ If you would like to provide feedback on OCI support or get access to early releases to test this functionality, + please get in touch. +
+
+
+
diff --git a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/HelmSection.tsx b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/HelmSection.tsx index 22790e781..e5d519174 100644 --- a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/HelmSection.tsx +++ b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/HelmSection.tsx @@ -4,6 +4,7 @@ import { TextTip } from '@@/Tip/TextTip'; import { FormControl } from '@@/form-components/FormControl'; import { FormSection } from '@@/form-components/FormSection'; import { Input } from '@@/form-components/Input'; +import { InsightsBox } from '@@/InsightsBox'; export function HelmSection() { const [{ name }, { error }] = useField('helmRepositoryUrl'); @@ -24,13 +25,34 @@ export function HelmSection() {
+ + At present Portainer does not support OCI format Helm charts. + Support for OCI charts will be available in a future release. If you + would like to provide feedback on OCI support or get access to early + releases to test this functionality,{' '} + + please get in touch + + . + + } + className="block w-fit mt-2 mb-1" + /> + diff --git a/pkg/libhelm/binary/install_test.go b/pkg/libhelm/binary/install_test.go index e3252e1f2..3d6c74707 100644 --- a/pkg/libhelm/binary/install_test.go +++ b/pkg/libhelm/binary/install_test.go @@ -53,11 +53,11 @@ func Test_Install(t *testing.T) { hbpm := NewHelmBinaryPackageManager(path) t.Run("successfully installs nginx chart with name test-nginx", func(t *testing.T) { - // helm install test-nginx --repo https://charts.bitnami.com/bitnami nginx + // helm install test-nginx --repo https://kubernetes.github.io/ingress-nginx nginx installOpts := options.InstallOptions{ Name: "test-nginx", Chart: "nginx", - Repo: "https://charts.bitnami.com/bitnami", + Repo: "https://kubernetes.github.io/ingress-nginx", } release, err := hbpm.Install(installOpts) @@ -67,10 +67,10 @@ func Test_Install(t *testing.T) { }) t.Run("successfully installs nginx chart with generated name", func(t *testing.T) { - // helm install --generate-name --repo https://charts.bitnami.com/bitnami nginx + // helm install --generate-name --repo https://kubernetes.github.io/ingress-nginx nginx installOpts := options.InstallOptions{ Chart: "nginx", - Repo: "https://charts.bitnami.com/bitnami", + Repo: "https://kubernetes.github.io/ingress-nginx", } release, err := hbpm.Install(installOpts) defer hbpm.run("uninstall", []string{release.Name}, nil) @@ -79,7 +79,7 @@ func Test_Install(t *testing.T) { }) t.Run("successfully installs nginx with values", func(t *testing.T) { - // helm install test-nginx-2 --repo https://charts.bitnami.com/bitnami nginx --values /tmp/helm-values3161785816 + // helm install test-nginx-2 --repo https://kubernetes.github.io/ingress-nginx nginx --values /tmp/helm-values3161785816 values, err := createValuesFile("service:\n port: 8081") is.NoError(err, "should create a values file") @@ -88,7 +88,7 @@ func Test_Install(t *testing.T) { installOpts := options.InstallOptions{ Name: "test-nginx-2", Chart: "nginx", - Repo: "https://charts.bitnami.com/bitnami", + Repo: "https://kubernetes.github.io/ingress-nginx", ValuesFile: values, } release, err := hbpm.Install(installOpts) diff --git a/pkg/libhelm/binary/search_repo_test.go b/pkg/libhelm/binary/search_repo_test.go index 4d5dd4d82..48786a0d7 100644 --- a/pkg/libhelm/binary/search_repo_test.go +++ b/pkg/libhelm/binary/search_repo_test.go @@ -22,7 +22,7 @@ func Test_SearchRepo(t *testing.T) { tests := []testCase{ {"not a helm repo", "https://portainer.io", true}, - {"bitnami helm repo", "https://charts.bitnami.com/bitnami", false}, + {"ingress helm repo", "https://kubernetes.github.io/ingress-nginx", false}, {"portainer helm repo", "https://portainer.github.io/k8s/", false}, {"gitlap helm repo with trailing slash", "https://charts.gitlab.io/", false}, {"elastic helm repo with trailing slash", "https://helm.elastic.co/", false}, diff --git a/pkg/libhelm/validate_repo_test.go b/pkg/libhelm/validate_repo_test.go index 7f81311ba..dce1257d0 100644 --- a/pkg/libhelm/validate_repo_test.go +++ b/pkg/libhelm/validate_repo_test.go @@ -26,7 +26,7 @@ func Test_ValidateHelmRepositoryURL(t *testing.T) { {"not helm repo", "http://google.com", true}, {"not valid repo with trailing slash", "http://google.com/", true}, {"not valid repo with trailing slashes", "http://google.com////", true}, - {"bitnami helm repo", "https://charts.bitnami.com/bitnami/", false}, + {"ingress helm repo", "https://kubernetes.github.io/ingress-nginx/", false}, {"gitlap helm repo", "https://charts.gitlab.io/", false}, {"portainer helm repo", "https://portainer.github.io/k8s/", false}, {"elastic helm repo", "https://helm.elastic.co/", false}, From e45b852c0948c0d24656224f1667c0d3108cdaa4 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Wed, 12 Feb 2025 09:23:52 +1300 Subject: [PATCH 019/212] fix(platform): remove error log when local env is not found [BE-11353] (#364) --- api/http/handler/system/system_info.go | 8 +++++++- api/http/handler/system/system_upgrade.go | 7 +++++-- api/platform/service.go | 10 +++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/api/http/handler/system/system_info.go b/api/http/handler/system/system_info.go index ad5f944ef..64a915313 100644 --- a/api/http/handler/system/system_info.go +++ b/api/http/handler/system/system_info.go @@ -3,6 +3,7 @@ package system import ( "net/http" + "github.com/pkg/errors" "github.com/portainer/portainer/api/internal/endpointutils" plf "github.com/portainer/portainer/api/platform" httperror "github.com/portainer/portainer/pkg/libhttp/error" @@ -46,7 +47,12 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http platform, err := handler.platformService.GetPlatform() if err != nil { - return httperror.InternalServerError("Failed to get platform", err) + if !errors.Is(err, plf.ErrNoLocalEnvironment) { + return httperror.InternalServerError("Failed to get platform", err) + } + // If no local environment is detected, we assume the platform is Docker + // UI will stop showing the upgrade banner + platform = plf.PlatformDocker } return response.JSON(w, &systemInfoResponse{ diff --git a/api/http/handler/system/system_upgrade.go b/api/http/handler/system/system_upgrade.go index f881b6233..8eb41413f 100644 --- a/api/http/handler/system/system_upgrade.go +++ b/api/http/handler/system/system_upgrade.go @@ -4,6 +4,7 @@ import ( "net/http" "regexp" + ceplf "github.com/portainer/portainer/api/platform" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/response" @@ -45,6 +46,9 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h environment, err := handler.platformService.GetLocalEnvironment() if err != nil { + if errors.Is(err, ceplf.ErrNoLocalEnvironment) { + return httperror.NotFound("The system upgrade feature is disabled because no local environment was detected.", err) + } return httperror.InternalServerError("Failed to get local environment", err) } @@ -53,8 +57,7 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h return httperror.InternalServerError("Failed to get platform", err) } - err = handler.upgradeService.Upgrade(platform, environment, payload.License) - if err != nil { + if err := handler.upgradeService.Upgrade(platform, environment, payload.License); err != nil { return httperror.InternalServerError("Failed to upgrade Portainer", err) } diff --git a/api/platform/service.go b/api/platform/service.go index 628e1cf9b..e74855fb7 100644 --- a/api/platform/service.go +++ b/api/platform/service.go @@ -14,6 +14,10 @@ import ( "github.com/rs/zerolog/log" ) +var ( + ErrNoLocalEnvironment = errors.New("No local environment was detected") +) + type Service interface { GetLocalEnvironment() (*portainer.Endpoint, error) GetPlatform() (ContainerPlatform, error) @@ -35,7 +39,7 @@ func (service *service) loadEnvAndPlatform() error { return nil } - environment, platform, err := guessLocalEnvironment(service.dataStore) + environment, platform, err := detectLocalEnvironment(service.dataStore) if err != nil { return err } @@ -73,7 +77,7 @@ var platformToEndpointType = map[ContainerPlatform][]portainer.EndpointType{ PlatformKubernetes: {portainer.KubernetesLocalEnvironment}, } -func guessLocalEnvironment(dataStore dataservices.DataStore) (*portainer.Endpoint, ContainerPlatform, error) { +func detectLocalEnvironment(dataStore dataservices.DataStore) (*portainer.Endpoint, ContainerPlatform, error) { platform := DetermineContainerPlatform() if !slices.Contains([]ContainerPlatform{PlatformDocker, PlatformKubernetes}, platform) { @@ -113,7 +117,7 @@ func guessLocalEnvironment(dataStore dataservices.DataStore) (*portainer.Endpoin } } - return nil, "", errors.New("failed to find local environment") + return nil, "", ErrNoLocalEnvironment } func checkDockerEnvTypeForUpgrade(environment *portainer.Endpoint) ContainerPlatform { From 96b1869a0cc74310ef49de148aa6c3c1a9b7a759 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:47:45 -0300 Subject: [PATCH 020/212] fix(swarm): fix the Host field when listing images BE-10827 (#352) Co-authored-by: andres-portainer Co-authored-by: LP B --- api/docker/client/client.go | 18 +++++++------- api/http/handler/docker/images/images_list.go | 24 +++++++++++-------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/api/docker/client/client.go b/api/docker/client/client.go index 065a40382..1161c4995 100644 --- a/api/docker/client/client.go +++ b/api/docker/client/client.go @@ -3,8 +3,8 @@ package client import ( "bytes" "errors" + "fmt" "io" - "maps" "net/http" "strings" "time" @@ -141,7 +141,6 @@ func createAgentClient(endpoint *portainer.Endpoint, endpointURL string, signatu type NodeNameTransport struct { *http.Transport - nodeNames map[string]string } func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) { @@ -176,18 +175,19 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) return resp, nil } - t.nodeNames = make(map[string]string) - for _, r := range rs { - t.nodeNames[r.ID] = r.Portainer.Agent.NodeName + nodeNames, ok := req.Context().Value("nodeNames").(map[string]string) + if ok { + for idx, r := range rs { + // as there is no way to differentiate the same image available in multiple nodes only by their ID + // we append the index of the image in the payload response to match the node name later + // from the image.Summary[] list returned by docker's client.ImageList() + nodeNames[fmt.Sprintf("%s-%d", r.ID, idx)] = r.Portainer.Agent.NodeName + } } return resp, err } -func (t *NodeNameTransport) NodeNames() map[string]string { - return maps.Clone(t.nodeNames) -} - func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) { transport := &NodeNameTransport{ Transport: &http.Transport{}, diff --git a/api/http/handler/docker/images/images_list.go b/api/http/handler/docker/images/images_list.go index ac7e980a0..eca99d993 100644 --- a/api/http/handler/docker/images/images_list.go +++ b/api/http/handler/docker/images/images_list.go @@ -1,10 +1,11 @@ package images import ( + "context" + "fmt" "net/http" "strings" - "github.com/portainer/portainer/api/docker/client" "github.com/portainer/portainer/api/http/handler/docker/utils" "github.com/portainer/portainer/api/set" httperror "github.com/portainer/portainer/pkg/libhttp/error" @@ -46,17 +47,16 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http return httpErr } - images, err := cli.ImageList(r.Context(), image.ListOptions{}) + nodeNames := make(map[string]string) + + // Pass the node names map to the context so the custom NodeNameTransport can use it + ctx := context.WithValue(r.Context(), "nodeNames", nodeNames) + + images, err := cli.ImageList(ctx, image.ListOptions{}) if err != nil { return httperror.InternalServerError("Unable to retrieve Docker images", err) } - // Extract the node name from the custom transport - nodeNames := make(map[string]string) - if t, ok := cli.HTTPClient().Transport.(*client.NodeNameTransport); ok { - nodeNames = t.NodeNames() - } - withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true) if err != nil { return httperror.BadRequest("Invalid query parameter: withUsage", err) @@ -85,8 +85,12 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http } imagesList[i] = ImageResponse{ - Created: image.Created, - NodeName: nodeNames[image.ID], + Created: image.Created, + // Only works if the order of `images` is not changed between unmarshaling the agent's response + // in NodeNameTransport.RoundTrip() (api/docker/client/client.go) + // and docker's cli.ImageList() + // As both functions unmarshal the same response body, the resulting array will be ordered the same way. + NodeName: nodeNames[fmt.Sprintf("%s-%d", image.ID, i)], ID: image.ID, Size: image.Size, Tags: image.RepoTags, From df8673ba405ce4147574f31e0fcb45c68bf94cb7 Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Fri, 14 Feb 2025 08:39:02 +1300 Subject: [PATCH 021/212] version: bump version to 2.27.0-rc3 - develop (#426) --- api/datastore/test_data/output_24_to_latest.json | 4 ++-- api/portainer.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index d9b9bf036..d1c264094 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -610,7 +610,7 @@ "RequiredPasswordLength": 12 }, "KubeconfigExpiry": "0", - "KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc2", + "KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc3", "LDAPSettings": { "AnonymousMode": true, "AutoCreateUsers": true, @@ -943,7 +943,7 @@ } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.27.0-rc2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.27.0-rc3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" }, "webhooks": null } \ No newline at end of file diff --git a/api/portainer.go b/api/portainer.go index 9a6d719e8..dd48936ed 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1636,7 +1636,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.27.0-rc2" + APIVersion = "2.27.0-rc3" // 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 From 41c1d8861571fb686e1672eac7aba8d9a91562b7 Mon Sep 17 00:00:00 2001 From: Viktor Pettersson Date: Wed, 19 Feb 2025 02:46:39 +0100 Subject: [PATCH 022/212] fix(edge): configure persisted mTLS certificates on start-up [BE-11622] (#437) Co-authored-by: andres-portainer Co-authored-by: oscarzhou Co-authored-by: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> --- api/filesystem/filesystem.go | 45 +++++++++++++++++++++++++----------- api/portainer.go | 3 ++- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index df49a2706..43e38b4e7 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -841,11 +841,11 @@ func (service *Service) GetDefaultSSLCertsPath() (string, string) { } func defaultMTLSCertPathUnderFileStore() (string, string, string) { - certPath := JoinPaths(SSLCertPath, MTLSCertFilename) caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename) + certPath := JoinPaths(SSLCertPath, MTLSCertFilename) keyPath := JoinPaths(SSLCertPath, MTLSKeyFilename) - return certPath, caCertPath, keyPath + return caCertPath, certPath, keyPath } // GetDefaultChiselPrivateKeyPath returns the chisle private key path @@ -1014,26 +1014,45 @@ func CreateFile(path string, r io.Reader) error { return err } -func (service *Service) StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error) { - certPath, caCertPath, keyPath := defaultMTLSCertPathUnderFileStore() +func (service *Service) StoreMTLSCertificates(caCert, cert, key []byte) (string, string, string, error) { + caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore() - r := bytes.NewReader(cert) - err := service.createFileInStore(certPath, r) - if err != nil { + r := bytes.NewReader(caCert) + if err := service.createFileInStore(caCertPath, r); err != nil { return "", "", "", err } - r = bytes.NewReader(caCert) - err = service.createFileInStore(caCertPath, r) - if err != nil { + r = bytes.NewReader(cert) + if err := service.createFileInStore(certPath, r); err != nil { return "", "", "", err } r = bytes.NewReader(key) - err = service.createFileInStore(keyPath, r) - if err != nil { + if err := service.createFileInStore(keyPath, r); err != nil { return "", "", "", err } - return service.wrapFileStore(certPath), service.wrapFileStore(caCertPath), service.wrapFileStore(keyPath), nil + return service.wrapFileStore(caCertPath), service.wrapFileStore(certPath), service.wrapFileStore(keyPath), nil +} + +func (service *Service) GetMTLSCertificates() (string, string, string, error) { + caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore() + + caCertPath = service.wrapFileStore(caCertPath) + certPath = service.wrapFileStore(certPath) + keyPath = service.wrapFileStore(keyPath) + + paths := [...]string{caCertPath, certPath, keyPath} + for _, path := range paths { + exists, err := service.FileExists(path) + if err != nil { + return "", "", "", err + } + + if !exists { + return "", "", "", fmt.Errorf("file %s does not exist", path) + } + } + + return caCertPath, certPath, keyPath, nil } diff --git a/api/portainer.go b/api/portainer.go index dd48936ed..2f9af995f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1491,7 +1491,8 @@ type ( StoreSSLCertPair(cert, key []byte) (string, string, error) CopySSLCertPair(certPath, keyPath string) (string, string, error) CopySSLCACert(caCertPath string) (string, error) - StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error) + StoreMTLSCertificates(caCert, cert, key []byte) (string, string, string, error) + GetMTLSCertificates() (string, string, string, error) GetDefaultChiselPrivateKeyPath() string StoreChiselPrivateKey(privateKey []byte) error } From cf95d91db39045adc83f566b7fe5c0a01632e314 Mon Sep 17 00:00:00 2001 From: LP B Date: Wed, 19 Feb 2025 19:25:28 +0100 Subject: [PATCH 023/212] fix(swarm): keep swarm stack stop command attached (#444) --- api/exec/swarm_stack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 634194d67..b5e13dcc6 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -127,7 +127,7 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta return err } - args = append(args, "stack", "rm", stack.Name) + args = append(args, "stack", "rm", "--detach=false", stack.Name) return runCommandAndCaptureStdErr(command, args, nil, "") } From 1b83542d41eb8f8acbc9c27aea355a31f64c9451 Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Thu, 20 Feb 2025 09:42:52 +1300 Subject: [PATCH 024/212] chore: bump version to 2.27.0 - develop (#445) --- api/datastore/test_data/output_24_to_latest.json | 4 ++-- api/http/handler/handler.go | 2 +- api/portainer.go | 2 +- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index d1c264094..a9ebeffb5 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -610,7 +610,7 @@ "RequiredPasswordLength": 12 }, "KubeconfigExpiry": "0", - "KubectlShellImage": "portainer/kubectl-shell:2.27.0-rc3", + "KubectlShellImage": "portainer/kubectl-shell:2.27.0", "LDAPSettings": { "AnonymousMode": true, "AutoCreateUsers": true, @@ -943,7 +943,7 @@ } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.27.0-rc3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.27.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" }, "webhooks": null } \ No newline at end of file diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 4325d9f91..4877e76c7 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -81,7 +81,7 @@ type Handler struct { } // @title PortainerCE API -// @version 2.26.0 +// @version 2.27.0 // @description.markdown api-description.md // @termsOfService diff --git a/api/portainer.go b/api/portainer.go index 2f9af995f..1195737d8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1637,7 +1637,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.27.0-rc3" + APIVersion = "2.27.0" // 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 diff --git a/package.json b/package.json index d9a9dcd6d..41d07244e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "2.26.0", + "version": "2.27.0", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" From 5d568a3f3290f3e1b945fc0d9ff103238c14e45a Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:09:26 +1300 Subject: [PATCH 025/212] fix(edge): edge stack pending when yaml file is under same root folder of edge configs [BE-11620] (#447) --- api/filesystem/serialize_per_dev_configs.go | 32 ++++--------------- .../serialize_per_dev_configs_test.go | 21 ++++++++++++ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/api/filesystem/serialize_per_dev_configs.go b/api/filesystem/serialize_per_dev_configs.go index c9d02ad0d..55881885a 100644 --- a/api/filesystem/serialize_per_dev_configs.go +++ b/api/filesystem/serialize_per_dev_configs.go @@ -44,11 +44,10 @@ func deduplicate(dirEntries []DirEntry) []DirEntry { // FilterDirForPerDevConfigs filers the given dirEntries, returns entries for the given device // For given configPath A/B/C, return entries: -// 1. all entries outside of dir A -// 2. dir entries A, A/B, A/B/C -// 3. For filterType file: +// 1. all entries outside of dir A/B/C +// 2. For filterType file: // file entries: A/B/C/ and A/B/C/.* -// 4. For filterType dir: +// 3. For filterType dir: // dir entry: A/B/C/ // all entries: A/B/C//* func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) []DirEntry { @@ -66,12 +65,7 @@ func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath str func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool { // Include all entries outside of dir A - if !isInConfigRootDir(dirEntry, configPath) { - return true - } - - // Include dir entries A, A/B, A/B/C - if isParentDir(dirEntry, configPath) { + if !isInConfigDir(dirEntry, configPath) { return true } @@ -90,21 +84,9 @@ func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filter return false } -func isInConfigRootDir(dirEntry DirEntry, configPath string) bool { - // get the first element of the configPath - rootDir := strings.Split(configPath, string(os.PathSeparator))[0] - - // return true if entry name starts with "A/" - return strings.HasPrefix(dirEntry.Name, appendTailSeparator(rootDir)) -} - -func isParentDir(dirEntry DirEntry, configPath string) bool { - if dirEntry.IsFile { - return false - } - - // return true for dir entries A, A/B, A/B/C - return strings.HasPrefix(appendTailSeparator(configPath), appendTailSeparator(dirEntry.Name)) +func isInConfigDir(dirEntry DirEntry, configPath string) bool { + // return true if entry name starts with "A/B" + return strings.HasPrefix(dirEntry.Name, appendTailSeparator(configPath)) } func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool { diff --git a/api/filesystem/serialize_per_dev_configs_test.go b/api/filesystem/serialize_per_dev_configs_test.go index 6a2a5f33b..55a1a4643 100644 --- a/api/filesystem/serialize_per_dev_configs_test.go +++ b/api/filesystem/serialize_per_dev_configs_test.go @@ -90,3 +90,24 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) { }) } } + +func TestIsInConfigDir(t *testing.T) { + f := func(dirEntry DirEntry, configPath string, expect bool) { + t.Helper() + + actual := isInConfigDir(dirEntry, configPath) + assert.Equal(t, expect, actual) + } + + f(DirEntry{Name: "edge-configs"}, "edge-configs", false) + f(DirEntry{Name: "edge-configs_backup"}, "edge-configs", false) + f(DirEntry{Name: "edge-configs/standalone-edge-agent-standard"}, "edge-configs", true) + f(DirEntry{Name: "parent/edge-configs/"}, "edge-configs", false) + f(DirEntry{Name: "edgestacktest"}, "edgestacktest/edge-configs", false) + f(DirEntry{Name: "edgestacktest/edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false) + f(DirEntry{Name: "edgestacktest/file1.conf"}, "edgestacktest/edge-configs", false) + f(DirEntry{Name: "edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false) + f(DirEntry{Name: "edgestacktest/edge-configs"}, "edgestacktest/edge-configs", false) + f(DirEntry{Name: "edgestacktest/edge-configs/standalone-edge-agent-async"}, "edgestacktest/edge-configs", true) + f(DirEntry{Name: "edgestacktest/edge-configs/abc.txt"}, "edgestacktest/edge-configs", true) +} From 9c243cc8dd4427c9158c76414f35b93685a5be96 Mon Sep 17 00:00:00 2001 From: James Carppe <85850129+jamescarppe@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:38:26 +1300 Subject: [PATCH 026/212] Update bug report template for 2.27.0 (#450) --- .github/ISSUE_TEMPLATE/bug_report.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e1ab32632..e2eef4f66 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -95,6 +95,7 @@ body: description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. 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.27.0' - '2.26.1' - '2.26.0' - '2.25.1' @@ -119,10 +120,6 @@ body: - '2.19.2' - '2.19.1' - '2.19.0' - - '2.18.4' - - '2.18.3' - - '2.18.2' - - '2.18.1' validations: required: true From cc73b7831f13b48fdfbbcd80be11add8a5324bc6 Mon Sep 17 00:00:00 2001 From: Steven Kang Date: Tue, 25 Feb 2025 12:55:44 +1300 Subject: [PATCH 027/212] fix: cve-2024-50338 - develop (#461) --- binary-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binary-version.json b/binary-version.json index 914838f53..ff017d023 100644 --- a/binary-version.json +++ b/binary-version.json @@ -2,5 +2,5 @@ "docker": "v27.5.1", "helm": "v3.17.0", "kubectl": "v1.32.1", - "mingit": "2.47.0.1" + "mingit": "2.48.1.1" } From dd98097897fc2dbe437aee24642d8f3a77c12194 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:00:25 +1300 Subject: [PATCH 028/212] fix(libstack): miss to read default .env file [BE-11638] (#458) --- pkg/libstack/compose/composeplugin.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/libstack/compose/composeplugin.go b/pkg/libstack/compose/composeplugin.go index 3162c6ab7..c83685351 100644 --- a/pkg/libstack/compose/composeplugin.go +++ b/pkg/libstack/compose/composeplugin.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "maps" + "os" "path/filepath" "slices" "strconv" @@ -87,7 +88,7 @@ func withComposeService( return composeFn(composeService, nil) } - env, err := parseEnvironment(options) + env, err := parseEnvironment(options, filePaths) if err != nil { return err } @@ -326,7 +327,7 @@ func addServiceLabels(project *types.Project, oneOff bool, edgeStackID portainer } } -func parseEnvironment(options libstack.Options) (map[string]string, error) { +func parseEnvironment(options libstack.Options, filePaths []string) (map[string]string, error) { env := make(map[string]string) for _, envLine := range options.Env { @@ -339,7 +340,22 @@ func parseEnvironment(options libstack.Options) (map[string]string, error) { } if options.EnvFilePath == "" { - return env, nil + if len(filePaths) == 0 { + return env, nil + } + + defaultDotEnv := filepath.Join(filepath.Dir(filePaths[0]), ".env") + s, err := os.Stat(defaultDotEnv) + if os.IsNotExist(err) { + return env, nil + } + if err != nil { + return env, err + } + if s.IsDir() { + return env, nil + } + options.EnvFilePath = defaultDotEnv } e, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath}) From 7759d762abf92ead75994dd7d0d14ce658e43fee Mon Sep 17 00:00:00 2001 From: James Player Date: Wed, 26 Feb 2025 14:13:50 +1300 Subject: [PATCH 029/212] chore(react): Convert cluster details to react CE (#466) --- app/kubernetes/react/views/index.ts | 5 + app/kubernetes/views/cluster/cluster.html | 33 --- app/kubernetes/views/cluster/cluster.js | 8 - .../views/cluster/clusterController.js | 139 ------------ app/react-tools/test-mocks.ts | 36 +++ .../FormControl/FormControl.tsx | 20 +- .../ClusterResourceReservation.test.tsx | 214 ++++++++++++++++++ .../ClusterResourceReservation.tsx | 39 ++++ .../cluster/ClusterView/ClusterView.tsx | 33 +++ .../kubernetes/cluster/ClusterView/index.ts | 1 + .../cluster/ClusterView/queries/index.ts | 3 + .../queries/useClusterResourceLimitsQuery.ts | 49 ++++ .../useClusterResourceReservationQuery.ts | 25 ++ .../queries/useClusterResourceUsageQuery.ts | 46 ++++ .../useClusterResourceReservationData.tsx | 72 ++++++ .../cluster/HomeView/nodes.service.ts | 2 +- .../components/ResourceReservation.tsx | 129 +++++++++++ .../components/ResourceUsageItem.tsx | 13 +- app/react/kubernetes/metrics/types.ts | 8 +- .../NamespaceResourceReservation.tsx | 45 ++++ .../ResourceQuotaFormSection.tsx | 4 +- .../ResourceReservationUsage.tsx | 150 ------------ .../useNamespaceResourceReservationData.ts | 65 ++++++ app/react/kubernetes/utils.ts | 35 +++ 24 files changed, 829 insertions(+), 345 deletions(-) delete mode 100644 app/kubernetes/views/cluster/cluster.html delete mode 100644 app/kubernetes/views/cluster/cluster.js delete mode 100644 app/kubernetes/views/cluster/clusterController.js create mode 100644 app/react/kubernetes/cluster/ClusterView/ClusterResourceReservation.test.tsx create mode 100644 app/react/kubernetes/cluster/ClusterView/ClusterResourceReservation.tsx create mode 100644 app/react/kubernetes/cluster/ClusterView/ClusterView.tsx create mode 100644 app/react/kubernetes/cluster/ClusterView/index.ts create mode 100644 app/react/kubernetes/cluster/ClusterView/queries/index.ts create mode 100644 app/react/kubernetes/cluster/ClusterView/queries/useClusterResourceLimitsQuery.ts create mode 100644 app/react/kubernetes/cluster/ClusterView/queries/useClusterResourceReservationQuery.ts create mode 100644 app/react/kubernetes/cluster/ClusterView/queries/useClusterResourceUsageQuery.ts create mode 100644 app/react/kubernetes/cluster/ClusterView/useClusterResourceReservationData.tsx create mode 100644 app/react/kubernetes/components/ResourceReservation.tsx rename app/react/kubernetes/{namespaces => }/components/ResourceUsageItem.tsx (75%) create mode 100644 app/react/kubernetes/namespaces/components/NamespaceForm/ResourceQuotaFormSection/NamespaceResourceReservation.tsx delete mode 100644 app/react/kubernetes/namespaces/components/NamespaceForm/ResourceQuotaFormSection/ResourceReservationUsage.tsx create mode 100644 app/react/kubernetes/namespaces/components/NamespaceForm/ResourceQuotaFormSection/useNamespaceResourceReservationData.ts diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts index a6c440de3..7913f7641 100644 --- a/app/kubernetes/react/views/index.ts +++ b/app/kubernetes/react/views/index.ts @@ -22,6 +22,7 @@ import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView'; import { NamespaceView } from '@/react/kubernetes/namespaces/ItemView/NamespaceView'; import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView'; import { JobsView } from '@/react/kubernetes/more-resources/JobsView/JobsView'; +import { ClusterView } from '@/react/kubernetes/cluster/ClusterView'; export const viewsModule = angular .module('portainer.kubernetes.react.views', []) @@ -78,6 +79,10 @@ export const viewsModule = angular [] ) ) + .component( + 'kubernetesClusterView', + r2a(withUIRouter(withReactQuery(withCurrentUser(ClusterView))), []) + ) .component( 'kubernetesConfigureView', r2a(withUIRouter(withReactQuery(withCurrentUser(ConfigureView))), []) diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html deleted file mode 100644 index 46acbf931..000000000 --- a/app/kubernetes/views/cluster/cluster.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - -
-
-
- - - -
- - -
- -
-
-
-
- -
- -
-
diff --git a/app/kubernetes/views/cluster/cluster.js b/app/kubernetes/views/cluster/cluster.js deleted file mode 100644 index 708076037..000000000 --- a/app/kubernetes/views/cluster/cluster.js +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesClusterView', { - templateUrl: './cluster.html', - controller: 'KubernetesClusterController', - controllerAs: 'ctrl', - bindings: { - endpoint: '<', - }, -}); diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js deleted file mode 100644 index b8f6df290..000000000 --- a/app/kubernetes/views/cluster/clusterController.js +++ /dev/null @@ -1,139 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash-es'; -import filesizeParser from 'filesize-parser'; -import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; -import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; -import { getMetricsForAllNodes, getTotalResourcesForAllApplications } from '@/react/kubernetes/metrics/metrics.ts'; - -class KubernetesClusterController { - /* @ngInject */ - constructor($async, $state, Notifications, LocalStorage, Authentication, KubernetesNodeService, KubernetesApplicationService, KubernetesEndpointService, EndpointService) { - this.$async = $async; - this.$state = $state; - this.Authentication = Authentication; - this.Notifications = Notifications; - this.LocalStorage = LocalStorage; - this.KubernetesNodeService = KubernetesNodeService; - this.KubernetesApplicationService = KubernetesApplicationService; - this.KubernetesEndpointService = KubernetesEndpointService; - this.EndpointService = EndpointService; - - this.onInit = this.onInit.bind(this); - this.getNodes = this.getNodes.bind(this); - this.getNodesAsync = this.getNodesAsync.bind(this); - this.getApplicationsAsync = this.getApplicationsAsync.bind(this); - this.getEndpointsAsync = this.getEndpointsAsync.bind(this); - this.hasResourceUsageAccess = this.hasResourceUsageAccess.bind(this); - } - - async getEndpointsAsync() { - try { - const endpoints = await this.KubernetesEndpointService.get(); - const systemEndpoints = _.filter(endpoints, { Namespace: 'kube-system' }); - this.systemEndpoints = _.filter(systemEndpoints, (ep) => ep.HolderIdentity); - - const kubernetesEndpoint = _.find(endpoints, { Name: 'kubernetes' }); - if (kubernetesEndpoint && kubernetesEndpoint.Subsets) { - const ips = _.flatten(_.map(kubernetesEndpoint.Subsets, 'Ips')); - _.forEach(this.nodes, (node) => { - node.Api = _.includes(ips, node.IPAddress); - }); - } - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve environments'); - } - } - - getEndpoints() { - return this.$async(this.getEndpointsAsync); - } - - async getNodesAsync() { - try { - const nodes = await this.KubernetesNodeService.get(); - _.forEach(nodes, (node) => (node.Memory = filesizeParser(node.Memory))); - this.nodes = nodes; - this.CPULimit = _.reduce(this.nodes, (acc, node) => node.CPU + acc, 0); - this.CPULimit = Math.round(this.CPULimit * 10000) / 10000; - this.MemoryLimit = _.reduce(this.nodes, (acc, node) => KubernetesResourceReservationHelper.megaBytesValue(node.Memory) + acc, 0); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve nodes'); - } - } - - getNodes() { - return this.$async(this.getNodesAsync); - } - - async getApplicationsAsync() { - try { - this.state.applicationsLoading = true; - - const applicationsResources = await getTotalResourcesForAllApplications(this.endpoint.Id); - this.resourceReservation = new KubernetesResourceReservation(); - this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest / 1000); - this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(applicationsResources.MemoryRequest); - - if (this.hasResourceUsageAccess()) { - await this.getResourceUsage(this.endpoint.Id); - } - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve applications'); - } finally { - this.state.applicationsLoading = false; - } - } - - getApplications() { - return this.$async(this.getApplicationsAsync); - } - - async getResourceUsage(endpointId) { - try { - const nodeMetrics = await getMetricsForAllNodes(endpointId); - const resourceUsageList = nodeMetrics.items.map((i) => i.usage); - const clusterResourceUsage = resourceUsageList.reduce((total, u) => { - total.CPU += KubernetesResourceReservationHelper.parseCPU(u.cpu); - total.Memory += KubernetesResourceReservationHelper.megaBytesValue(u.memory); - return total; - }, new KubernetesResourceReservation()); - this.resourceUsage = clusterResourceUsage; - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve cluster resource usage'); - } - } - - /** - * Check if resource usage stats can be displayed - * @returns {boolean} - */ - hasResourceUsageAccess() { - return this.isAdmin && this.state.useServerMetrics; - } - - async onInit() { - this.endpoint = await this.EndpointService.endpoint(this.endpoint.Id); - this.isAdmin = this.Authentication.isAdmin(); - const useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; - - this.state = { - applicationsLoading: true, - viewReady: false, - useServerMetrics, - }; - - await this.getNodes(); - if (this.isAdmin) { - await Promise.allSettled([this.getEndpoints(), this.getApplicationsAsync()]); - } - - this.state.viewReady = true; - } - - $onInit() { - return this.$async(this.onInit); - } -} - -export default KubernetesClusterController; -angular.module('portainer.kubernetes').controller('KubernetesClusterController', KubernetesClusterController); diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 556d7e4c2..20fe7dee3 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { QueryObserverResult } from '@tanstack/react-query'; import { Team } from '@/react/portainer/users/teams/types'; import { Role, User, UserId } from '@/portainer/users/types'; @@ -134,3 +135,38 @@ export function createMockEnvironment(): Environment { }, }; } + +export function createMockQueryResult( + data: TData, + overrides?: Partial> +) { + const defaultResult = { + data, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + errorUpdateCount: 0, + failureReason: null, + isError: false, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: false, + isPaused: false, + isPlaceholderData: false, + isPreviousData: false, + isRefetchError: false, + isRefetching: false, + isStale: false, + isSuccess: true, + refetch: async () => defaultResult, + remove: () => {}, + status: 'success', + fetchStatus: 'idle', + }; + + return { ...defaultResult, ...overrides }; +} diff --git a/app/react/components/form-components/FormControl/FormControl.tsx b/app/react/components/form-components/FormControl/FormControl.tsx index 5f4c8727a..7c7151dd6 100644 --- a/app/react/components/form-components/FormControl/FormControl.tsx +++ b/app/react/components/form-components/FormControl/FormControl.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; +import { PropsWithChildren, ReactNode } from 'react'; import clsx from 'clsx'; import { Tooltip } from '@@/Tip/Tooltip'; @@ -10,10 +10,11 @@ export type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'vertical'; export interface Props { inputId?: string; + dataCy?: string; label: ReactNode; size?: Size; - tooltip?: ComponentProps['message']; - setTooltipHtmlMessage?: ComponentProps['setHtmlMessage']; + tooltip?: ReactNode; + setTooltipHtmlMessage?: boolean; children: ReactNode; errors?: ReactNode; required?: boolean; @@ -24,6 +25,7 @@ export interface Props { export function FormControl({ inputId, + dataCy, label, size = 'small', tooltip = '', @@ -42,6 +44,7 @@ export function FormControl({ 'form-group', 'after:clear-both after:table after:content-[""]' // to fix issues with float )} + data-cy={dataCy} >