From 1543ad4c4217006b66c2fc00d52c2243780bb4c4 Mon Sep 17 00:00:00 2001 From: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:40:10 +1200 Subject: [PATCH 01/26] fix(k8s): fixed apply a note to k8s application (#5586) --- app/kubernetes/converters/deployment.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js index 080644641..0d7705441 100644 --- a/app/kubernetes/converters/deployment.js +++ b/app/kubernetes/converters/deployment.js @@ -54,10 +54,15 @@ class KubernetesDeploymentConverter { payload.spec.template.metadata.labels.app = deployment.Name; payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; payload.spec.template.spec.containers[0].name = deployment.Name; - payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel); - if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) { - payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }]; + + if (deployment.ImageModel) { + payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel); + + if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) { + payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }]; + } } + payload.spec.template.spec.affinity = deployment.Affinity; KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env); KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts); From 9f179fe3ecf11c039aad5aad7e8ca7975aae5326 Mon Sep 17 00:00:00 2001 From: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Date: Wed, 8 Sep 2021 20:42:17 +1200 Subject: [PATCH 02/26] feat(ui):rename endpoint(s) to environment(s) EE-1206 (#5588) * rename endpoints to environments EE-1206 --- api/bolt/init.go | 2 +- api/bolt/migrator/migrate_dbversion31.go | 4 +-- api/chisel/service.go | 8 ++--- api/cli/cli.go | 6 ++-- api/cmd/portainer/main.go | 8 ++--- api/docker/errors.go | 2 +- api/docker/snapshot.go | 16 ++++----- api/exec/compose_stack.go | 2 +- api/http/errors/errors.go | 2 +- .../handler/edgegroups/edgegroup_create.go | 4 +-- .../handler/edgegroups/edgegroup_inspect.go | 2 +- api/http/handler/edgegroups/edgegroup_list.go | 2 +- .../handler/edgegroups/edgegroup_update.go | 10 +++--- api/http/handler/edgejobs/edgejob_create.go | 8 ++--- .../handler/edgestacks/edgestack_create.go | 8 ++--- .../handler/edgestacks/edgestack_delete.go | 8 ++--- .../edgestacks/edgestack_status_update.go | 8 ++--- .../handler/edgestacks/edgestack_update.go | 16 ++++----- .../endpointedge/endpoint_edgejob_logs.go | 8 ++--- .../endpoint_edgestack_inspect.go | 8 ++--- .../endpointgroups/endpointgroup_create.go | 10 +++--- .../endpointgroups/endpointgroup_delete.go | 16 ++++----- .../endpointgroup_endpoint_add.go | 16 ++++----- .../endpointgroup_endpoint_delete.go | 16 ++++----- .../endpointgroups/endpointgroup_inspect.go | 6 ++-- .../endpointgroups/endpointgroup_list.go | 2 +- .../endpointgroups/endpointgroup_update.go | 14 ++++---- api/http/handler/endpointproxy/proxy_azure.go | 8 ++--- .../handler/endpointproxy/proxy_docker.go | 10 +++--- .../handler/endpointproxy/proxy_kubernetes.go | 10 +++--- .../handler/endpointproxy/proxy_storidge.go | 10 +++--- .../endpoints/endpoint_association_delete.go | 10 +++--- api/http/handler/endpoints/endpoint_create.go | 22 ++++++------ api/http/handler/endpoints/endpoint_delete.go | 10 +++--- .../endpoints/endpoint_dockerhub_status.go | 6 ++-- .../endpoints/endpoint_extension_add.go | 8 ++--- .../endpoints/endpoint_extension_remove.go | 8 ++--- .../handler/endpoints/endpoint_inspect.go | 8 ++--- api/http/handler/endpoints/endpoint_list.go | 4 +-- .../endpoints/endpoint_registries_inspect.go | 2 +- .../endpoints/endpoint_registries_list.go | 8 ++--- .../endpoints/endpoint_registry_access.go | 12 +++---- .../endpoints/endpoint_settings_update.go | 8 ++--- .../handler/endpoints/endpoint_snapshot.go | 12 +++---- .../handler/endpoints/endpoint_snapshots.go | 8 ++--- .../endpoints/endpoint_status_inspect.go | 10 +++--- api/http/handler/endpoints/endpoint_update.go | 16 ++++----- api/http/handler/kubernetes/handler.go | 4 +-- .../handler/kubernetes/kubernetes_config.go | 8 ++--- .../kubernetes/kubernetes_nodes_limits.go | 6 ++-- .../kubernetes/namespaces_toggle_system.go | 2 +- api/http/handler/stacks/stack_create.go | 6 ++-- api/http/handler/stacks/stack_delete.go | 12 +++---- api/http/handler/stacks/stack_file.go | 6 ++-- api/http/handler/stacks/stack_inspect.go | 6 ++-- api/http/handler/stacks/stack_list.go | 2 +- api/http/handler/stacks/stack_migrate.go | 14 ++++---- api/http/handler/stacks/stack_start.go | 6 ++-- api/http/handler/stacks/stack_stop.go | 6 ++-- api/http/handler/stacks/stack_update.go | 6 ++-- api/http/handler/stacks/stack_update_git.go | 6 ++-- .../stacks/stack_update_git_redeploy.go | 6 ++-- api/http/handler/tags/tag_delete.go | 14 ++++---- api/http/handler/webhooks/webhook_execute.go | 4 +-- api/http/handler/websocket/attach.go | 6 ++-- api/http/handler/websocket/exec.go | 6 ++-- api/http/handler/websocket/pod.go | 6 ++-- api/http/handler/websocket/shell_pod.go | 6 ++-- api/http/middlewares/endpoint.go | 6 ++-- api/http/security/bouncer.go | 2 +- api/internal/snapshot/snapshot.go | 14 ++++---- api/kubernetes/cli/client.go | 4 +-- api/stacks/deploy.go | 2 +- api/stacks/deploy_test.go | 2 +- app/azure/_module.js | 2 +- app/docker/__module.js | 6 ++-- app/docker/views/dashboard/dashboard.html | 6 ++-- .../docker-features-configuration.html | 2 +- .../associatedEndpointsDatatable.html | 2 +- .../components/edge-job-form/edgeJobForm.html | 6 ++-- .../edgeJobResultsDatatable.html | 2 +- .../edgeStackEndpointsDatatable.html | 2 +- .../edgeStackEndpointsDatatableController.js | 2 +- .../edge-stack-status/edgeStackStatus.html | 6 ++-- app/edge/components/group-form/groupForm.html | 16 ++++----- .../groups-datatable/groupsDatatable.html | 2 +- .../edge-jobs/edgeJob/edgeJobController.js | 2 +- .../editEdgeStackView/editEdgeStackView.html | 4 +-- .../editEdgeStackViewController.js | 2 +- app/kubernetes/__module.js | 2 +- app/kubernetes/endpoint/service.js | 2 +- .../create/createApplication.html | 2 +- .../views/cluster/clusterController.js | 2 +- .../views/cluster/node/nodeController.js | 2 +- app/kubernetes/views/configure/configure.html | 2 +- .../views/configure/configureController.js | 2 +- app/kubernetes/views/dashboard/dashboard.html | 6 ++-- .../access/resourcePoolAccess.html | 4 +-- .../create/createResourcePool.html | 2 +- .../resource-pools/edit/resourcePool.html | 2 +- app/portainer/__module.js | 2 +- .../access-datatable/accessDatatable.html | 2 +- .../porAccessControlPanel.html | 4 +-- .../associatedEndpointsSelector.html | 10 +++--- .../accessViewerDatatable.html | 2 +- .../endpointsDatatable.html | 4 +-- .../roles-datatable/rolesDatatable.html | 10 +++--- .../stacks-datatable/stacksDatatable.html | 2 +- .../endpoint-list/endpointList.html | 2 +- .../endpoint-selector/endpointSelector.html | 2 +- .../endpointSecurity/porEndpointSecurity.html | 2 +- .../forms/group-form/groupForm.html | 18 +++++----- .../forms/group-form/groupFormController.js | 8 ++--- .../stackFromTemplateForm.html | 2 +- .../informationPanelOffline.html | 2 +- .../informationPanelOfflineController.js | 4 +-- app/portainer/services/api/endpointService.js | 8 ++--- app/portainer/services/api/stackService.js | 2 +- app/portainer/services/modalService.js | 8 ++--- app/portainer/services/stateManager.js | 2 +- .../ssl-certificate/ssl-certificate.html | 2 +- app/portainer/views/auth/authController.js | 2 +- .../endpoints/access/endpointAccess.html | 6 ++-- .../access/endpointAccessController.js | 2 +- .../create/createEndpointController.js | 16 ++++----- .../endpoints/create/createendpoint.html | 34 +++++++++---------- .../views/endpoints/edit/endpoint.html | 18 +++++----- .../endpoints/edit/endpointController.js | 10 +++--- app/portainer/views/endpoints/endpoints.html | 6 ++-- .../views/endpoints/endpointsController.js | 6 ++-- .../views/groups/access/groupAccess.html | 2 +- .../views/groups/create/creategroup.html | 4 +-- app/portainer/views/groups/edit/group.html | 2 +- app/portainer/views/groups/groups.html | 6 ++-- .../views/groups/groupsController.js | 4 +-- app/portainer/views/home/home.html | 10 +++--- app/portainer/views/home/homeController.js | 6 ++-- app/portainer/views/init/admin/initAdmin.html | 2 +- .../views/init/endpoint/initEndpoint.html | 16 ++++----- .../init/endpoint/initEndpointController.js | 2 +- .../views/registries/registries.html | 2 +- .../views/registries/registriesController.js | 2 +- app/portainer/views/roles/roles.html | 2 +- app/portainer/views/settings/settings.html | 2 +- app/portainer/views/sidebar/sidebar.html | 2 +- app/portainer/views/stacks/edit/stack.html | 6 ++-- .../views/stacks/edit/stackController.js | 8 ++--- app/portainer/views/users/edit/user.html | 2 +- app/portainer/views/users/users.html | 6 ++-- .../integration/ci_basic_tests/init.spec.js | 2 +- test/e2e/cypress/integration/init.spec.js | 2 +- 151 files changed, 474 insertions(+), 474 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 1c997087e..4df6b327b 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -80,7 +80,7 @@ func (store *Store) Init() error { if len(groups) == 0 { unassignedGroup := &portainer.EndpointGroup{ Name: "Unassigned", - Description: "Unassigned endpoints", + Description: "Unassigned environments", Labels: []portainer.Pair{}, UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, diff --git a/api/bolt/migrator/migrate_dbversion31.go b/api/bolt/migrator/migrate_dbversion31.go index 6c9f7f00b..e9a242f70 100644 --- a/api/bolt/migrator/migrate_dbversion31.go +++ b/api/bolt/migrator/migrate_dbversion31.go @@ -138,7 +138,7 @@ func (m *Migrator) updateDockerhubToDB32() error { func (m *Migrator) updateVolumeResourceControlToDB32() error { endpoints, err := m.endpointService.Endpoints() if err != nil { - return fmt.Errorf("failed fetching endpoints: %w", err) + return fmt.Errorf("failed fetching environments: %w", err) } resourceControls, err := m.resourceControlService.ResourceControls() @@ -170,7 +170,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error { endpointDockerID, err := snapshotutils.FetchDockerID(snapshot) if err != nil { - return fmt.Errorf("failed fetching endpoint docker id: %w", err) + return fmt.Errorf("failed fetching environment docker id: %w", err) } if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done { diff --git a/api/chisel/service.go b/api/chisel/service.go index d5787d9e5..9af0f4866 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -141,7 +141,7 @@ func (service *Service) checkTunnels() { } elapsed := time.Since(tunnel.LastActivity) - log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds()) + log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds()) if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() { continue @@ -156,19 +156,19 @@ func (service *Service) checkTunnels() { endpointID, err := strconv.Atoi(item.Key) if err != nil { - log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err) + log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err) } err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port) if err != nil { - log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err) + log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err) } } if len(tunnel.Jobs) > 0 { endpointID, err := strconv.Atoi(item.Key) if err != nil { - log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err) + log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err) continue } diff --git a/api/cli/cli.go b/api/cli/cli.go index 1da82bf63..e5c984807 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -18,7 +18,7 @@ import ( type Service struct{} var ( - errInvalidEndpointProtocol = errors.New("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") + errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://") errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe") errInvalidSnapshotInterval = errors.New("Invalid snapshot interval") errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file") @@ -35,7 +35,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), - EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), + EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(), EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), @@ -47,7 +47,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(), SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(), SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(), - SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), + SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(), AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index a6070ad25..00080dade 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -316,7 +316,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat err := snapshotService.SnapshotEndpoint(endpoint) if err != nil { - log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) } return dataStore.Endpoint().CreateEndpoint(endpoint) @@ -362,7 +362,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, err := snapshotService.SnapshotEndpoint(endpoint) if err != nil { - log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) } return dataStore.Endpoint().CreateEndpoint(endpoint) @@ -379,7 +379,7 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap } if len(endpoints) > 0 { - log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.") + log.Println("Instance already has defined environments. Skipping the environment defined via CLI.") return nil } @@ -474,7 +474,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { err = initEndpoint(flags, dataStore, snapshotService) if err != nil { - log.Fatalf("failed initializing endpoint: %v", err) + log.Fatalf("failed initializing environment: %v", err) } adminPasswordHash := "" diff --git a/api/docker/errors.go b/api/docker/errors.go index 80611c8b4..04e415934 100644 --- a/api/docker/errors.go +++ b/api/docker/errors.go @@ -4,5 +4,5 @@ import "errors" // Docker errors var ( - ErrUnableToPingEndpoint = errors.New("Unable to communicate with the endpoint") + ErrUnableToPingEndpoint = errors.New("Unable to communicate with the environment") ) diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index f3e96ff7e..5283acf1b 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -47,44 +47,44 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Dock err = snapshotInfo(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [environment: %s] [err: %s]", endpoint.Name, err) } if snapshot.Swarm { err = snapshotSwarmServices(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [environment: %s] [err: %s]", endpoint.Name, err) } err = snapshotNodes(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [environment: %s] [err: %s]", endpoint.Name, err) } } err = snapshotContainers(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [environment: %s] [err: %s]", endpoint.Name, err) } err = snapshotImages(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [environment: %s] [err: %s]", endpoint.Name, err) } err = snapshotVolumes(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [environment: %s] [err: %s]", endpoint.Name, err) } err = snapshotNetworks(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [environment: %s] [err: %s]", endpoint.Name, err) } err = snapshotVersion(snapshot, cli) if err != nil { - log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [endpoint: %s] [err: %s]", endpoint.Name, err) + log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [environment: %s] [err: %s]", endpoint.Name, err) } snapshot.Time = time.Now().Unix() diff --git a/api/exec/compose_stack.go b/api/exec/compose_stack.go index 94df99a65..087274fe9 100644 --- a/api/exec/compose_stack.go +++ b/api/exec/compose_stack.go @@ -46,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string { func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error { url, proxy, err := manager.fetchEndpointProxy(endpoint) if err != nil { - return errors.Wrap(err, "failed to featch endpoint proxy") + return errors.Wrap(err, "failed to featch environment proxy") } if proxy != nil { diff --git a/api/http/errors/errors.go b/api/http/errors/errors.go index 2e6aeceb5..ef0abcfee 100644 --- a/api/http/errors/errors.go +++ b/api/http/errors/errors.go @@ -4,7 +4,7 @@ import "errors" var ( // ErrEndpointAccessDenied Access denied to endpoint error - ErrEndpointAccessDenied = errors.New("Access denied to endpoint") + ErrEndpointAccessDenied = errors.New("Access denied to environment") // ErrUnauthorized Unauthorized error ErrUnauthorized = errors.New("Unauthorized") // ErrResourceAccessDenied Access denied to resource error diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index 81f832bb2..c461de77f 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -27,7 +27,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error { return errors.New("TagIDs is mandatory for a dynamic Edge group") } if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) { - return errors.New("Endpoints is mandatory for a static Edge group") + return errors.New("Environment is mandatory for a static Edge group") } return nil } @@ -77,7 +77,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) for _, endpointID := range payload.Endpoints { endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err} } if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { diff --git a/api/http/handler/edgegroups/edgegroup_inspect.go b/api/http/handler/edgegroups/edgegroup_inspect.go index 938624c29..e40d3989d 100644 --- a/api/http/handler/edgegroups/edgegroup_inspect.go +++ b/api/http/handler/edgegroups/edgegroup_inspect.go @@ -38,7 +38,7 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) if edgeGroup.Dynamic { endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments and environment groups for Edge group", err} } edgeGroup.Endpoints = endpoints diff --git a/api/http/handler/edgegroups/edgegroup_list.go b/api/http/handler/edgegroups/edgegroup_list.go index 5e56a35b7..efaf7dd85 100644 --- a/api/http/handler/edgegroups/edgegroup_list.go +++ b/api/http/handler/edgegroups/edgegroup_list.go @@ -51,7 +51,7 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h if edgeGroup.Dynamic { endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments and environment groups for Edge group", err} } edgeGroup.Endpoints = endpoints diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index 2a5105d70..f034325c6 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -29,7 +29,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error { return errors.New("TagIDs is mandatory for a dynamic Edge group") } if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) { - return errors.New("Endpoints is mandatory for a static Edge group") + return errors.New("Environments is mandatory for a static Edge group") } return nil } @@ -81,12 +81,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} } endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} } oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups) @@ -99,7 +99,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) for _, endpointID := range payload.Endpoints { endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err} } if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { @@ -124,7 +124,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) for _, endpointID := range endpointsToUpdate { err = handler.updateEndpoint(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Endpoint relation changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Environment relation changes inside the database", err} } } diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index 3e78914d5..efb826e66 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -66,7 +66,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er } if payload.Endpoints == nil || len(payload.Endpoints) == 0 { - return errors.New("Invalid endpoints payload") + return errors.New("Invalid environment payload") } if govalidator.IsNull(payload.FileContent) { @@ -119,9 +119,9 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error { payload.CronExpression = cronExpression var endpoints []portainer.EndpointID - err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false) + err = request.RetrieveMultiPartFormJSONValue(r, "Environments", &endpoints, false) if err != nil { - return errors.New("Invalid endpoints") + return errors.New("Invalid environments") } payload.Endpoints = endpoints @@ -206,7 +206,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file [] } if len(edgeJob.Endpoints) == 0 { - return errors.New("Endpoints are mandatory for an Edge job") + return errors.New("Environments are mandatory for an Edge job") } scriptPath, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), file) diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 06edc2278..80e838827 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -44,12 +44,12 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} } endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -62,14 +62,14 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) for _, endpointID := range relatedEndpoints { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} } relation.EdgeStacks[edgeStack.ID] = true err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err} } } diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index a1937e03f..7ff0ee1cd 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -44,12 +44,12 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} } endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -62,14 +62,14 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) for _, endpointID := range relatedEndpoints { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} } delete(relation.EdgeStacks, edgeStack.ID) err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err} } } diff --git a/api/http/handler/edgestacks/edgestack_status_update.go b/api/http/handler/edgestacks/edgestack_status_update.go index 3eaea47b7..0ac60b981 100644 --- a/api/http/handler/edgestacks/edgestack_status_update.go +++ b/api/http/handler/edgestacks/edgestack_status_update.go @@ -23,7 +23,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error { return errors.New("Invalid status") } if payload.EndpointID == nil { - return errors.New("Invalid EndpointID") + return errors.New("Invalid EnvironmentID") } if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) { return errors.New("Error message is mandatory when status is error") @@ -65,14 +65,14 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(*payload.EndpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{ diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index 695106bf6..e81fe5579 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -67,12 +67,12 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) if payload.EdgeGroups != nil { endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} } endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -82,12 +82,12 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) oldRelated, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} } newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} } oldRelatedSet := EndpointSet(oldRelated) @@ -103,14 +103,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) for endpointID := range endpointsToRemove { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} } delete(relation.EdgeStacks, stack.ID) err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err} } } @@ -124,14 +124,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) for endpointID := range endpointsToAdd { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} } relation.EdgeStacks[stack.ID] = true err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err} } } diff --git a/api/http/handler/endpointedge/endpoint_edgejob_logs.go b/api/http/handler/endpointedge/endpoint_edgejob_logs.go index 59f55298d..9626a7a3e 100644 --- a/api/http/handler/endpointedge/endpoint_edgejob_logs.go +++ b/api/http/handler/endpointedge/endpoint_edgejob_logs.go @@ -34,19 +34,19 @@ func (payload *logsPayload) Validate(r *http.Request) error { func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID") diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 9e1057b20..823f21c98 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -32,19 +32,19 @@ type configResponse struct { func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId") diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index eb19b9d9e..46879b694 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -24,7 +24,7 @@ type endpointGroupCreatePayload struct { func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return errors.New("Invalid endpoint group name") + return errors.New("Invalid environment group name") } if payload.TagIDs == nil { payload.TagIDs = []portainer.TagID{} @@ -61,12 +61,12 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque err = handler.DataStore.EndpointGroup().CreateEndpointGroup(endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the environment group inside the database", err} } endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } for _, id := range payload.AssociatedEndpoints { @@ -76,12 +76,12 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment", err} } err = handler.updateEndpointRelations(&endpoint, endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err} } break diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index 4edf260a3..baa58f307 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -28,28 +28,28 @@ import ( func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err} } if endpointGroupID == 1 { - return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default endpoint group")} + return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default environment group")} } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err} } err = handler.DataStore.EndpointGroup().DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the environment group from the database", err} } endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err} } for _, endpoint := range endpoints { @@ -57,12 +57,12 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque endpoint.GroupID = portainer.EndpointGroupID(1) err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment", err} } err = handler.updateEndpointRelations(&endpoint, nil) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err} } } } diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go index b28370827..fb0759e7e 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -26,38 +26,38 @@ import ( func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err} } endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } endpoint.GroupID = endpointGroup.ID err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } err = handler.updateEndpointRelations(endpoint, endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err} } return response.Empty(w) diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go index ecba9adaa..4963af012 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -25,38 +25,38 @@ import ( func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err} } endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } _, err = handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } endpoint.GroupID = portainer.EndpointGroupID(1) err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } err = handler.updateEndpointRelations(endpoint, nil) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err} } return response.Empty(w) diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index eb573c145..e25290669 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -26,14 +26,14 @@ import ( func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err} } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err} } return response.JSON(w, endpointGroup) diff --git a/api/http/handler/endpointgroups/endpointgroup_list.go b/api/http/handler/endpointgroups/endpointgroup_list.go index b011d6a2c..6cb0cfabe 100644 --- a/api/http/handler/endpointgroups/endpointgroup_list.go +++ b/api/http/handler/endpointgroups/endpointgroup_list.go @@ -23,7 +23,7 @@ import ( func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err} } securityContext, err := security.RetrieveRestrictedRequestContext(r) diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 7d8c284f4..ab60c289c 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -45,7 +45,7 @@ func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err} } var payload endpointGroupUpdatePayload @@ -56,9 +56,9 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err} } if payload.Name != "" { @@ -123,7 +123,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque if updateAuthorizations { endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } for _, endpoint := range endpoints { @@ -140,13 +140,13 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment group changes inside the database", err} } if tagsChanged { endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } @@ -154,7 +154,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque if endpoint.GroupID == endpointGroup.ID { err = handler.updateEndpointRelations(&endpoint, endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err} } } } diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index 8176edf8a..b838f0edc 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -14,19 +14,19 @@ import ( func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } var proxy http.Handler diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 9ac89edbc..0f0db7af6 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -17,24 +17,24 @@ import ( func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { if endpoint.EdgeID == "" { - return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} + return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")} } tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go index 82d922ca1..2e3323079 100644 --- a/api/http/handler/endpointproxy/proxy_kubernetes.go +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -17,24 +17,24 @@ import ( func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { if endpoint.EdgeID == "" { - return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} + return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")} } tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index 6cc59ff17..147845bc0 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -17,19 +17,19 @@ import ( func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } var storidgeExtension *portainer.EndpointExtension @@ -40,7 +40,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt } if storidgeExtension == nil { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", errors.New("This extension is not supported")} + return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this environment", errors.New("This extension is not supported")} } proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL diff --git a/api/http/handler/endpoints/endpoint_association_delete.go b/api/http/handler/endpoints/endpoint_association_delete.go index c7f9046c8..2ef6fe6c4 100644 --- a/api/http/handler/endpoints/endpoint_association_delete.go +++ b/api/http/handler/endpoints/endpoint_association_delete.go @@ -30,18 +30,18 @@ import ( func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint type", errors.New("Invalid endpoint type")} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")} } endpoint.EdgeID = "" @@ -55,7 +55,7 @@ func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting environment in database", err} } handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID) diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 50e462869..67f6086d1 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -53,13 +53,13 @@ const ( func (payload *endpointCreatePayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return errors.New("Invalid endpoint name") + return errors.New("Invalid environment name") } payload.Name = name endpointCreationType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointCreationType", false) if err != nil || endpointCreationType == 0 { - return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)") + return errors.New("Invalid environment type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)") } payload.EndpointCreationType = endpointCreationEnum(endpointCreationType) @@ -133,7 +133,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { default: endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { - return errors.New("Invalid endpoint URL") + return errors.New("Invalid environment URL") } payload.URL = endpointURL @@ -189,7 +189,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group inside the database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -238,7 +238,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain if payload.EndpointCreationType == agentEnvironment { agentPlatform, err := handler.pingAndCheckPlatform(payload) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get endpoint type", err} + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err} } if agentPlatform == portainer.AgentPlatformDocker { @@ -288,7 +288,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po err = handler.saveEndpointAndUpdateAuthorizations(endpoint) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} + return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err} } return endpoint, nil @@ -299,7 +299,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) portainerURL, err := url.Parse(payload.URL) if err != nil { - return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err} + return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", err} } portainerHost, _, err := net.SplitHostPort(portainerURL.Host) @@ -308,7 +308,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) } if portainerHost == "localhost" { - return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")} + return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", errors.New("cannot use localhost as environment URL")} } edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID) @@ -335,7 +335,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) err = handler.saveEndpointAndUpdateAuthorizations(endpoint) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} + return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err} } return endpoint, nil @@ -455,12 +455,12 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) if strings.Contains(err.Error(), "Invalid request signature") { err = errors.New("agent already paired with another Portainer instance") } - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with environment", err} } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err} } return nil diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index a2dcf1924..43032f881 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -26,14 +26,14 @@ import ( func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if endpoint.TLSConfig.TLS { @@ -46,14 +46,14 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * err = handler.DataStore.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID)) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err} } handler.ProxyManager.DeleteEndpointProxy(endpoint) err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment relation from the database", err} } for _, tagID := range endpoint.TagIDs { diff --git a/api/http/handler/endpoints/endpoint_dockerhub_status.go b/api/http/handler/endpoints/endpoint_dockerhub_status.go index 7a8cc44a3..5be4d39a4 100644 --- a/api/http/handler/endpoints/endpoint_dockerhub_status.go +++ b/api/http/handler/endpoints/endpoint_dockerhub_status.go @@ -26,14 +26,14 @@ type dockerhubStatusResponse struct { func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if !endpointutils.IsLocalEndpoint(endpoint) { diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index 36112e5ea..7e08fc1b8 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -32,14 +32,14 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error { func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } var payload endpointExtensionAddPayload @@ -69,7 +69,7 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } return response.JSON(w, extension) diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index 13e06a1f5..75d798cd7 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -15,14 +15,14 @@ import ( func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType") @@ -38,7 +38,7 @@ func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.R err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } return response.Empty(w) diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index bab2452f5..b538be794 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -26,19 +26,19 @@ import ( func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } hideFields(endpoint) diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 92fca9f63..5f8b1b5d6 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -60,12 +60,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err} } endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } settings, err := handler.DataStore.Settings().Settings() diff --git a/api/http/handler/endpoints/endpoint_registries_inspect.go b/api/http/handler/endpoints/endpoint_registries_inspect.go index 405fe9495..03c0fe66c 100644 --- a/api/http/handler/endpoints/endpoint_registries_inspect.go +++ b/api/http/handler/endpoints/endpoint_registries_inspect.go @@ -16,7 +16,7 @@ import ( func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err} } registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId") diff --git a/api/http/handler/endpoints/endpoint_registries_list.go b/api/http/handler/endpoints/endpoint_registries_list.go index 06309624f..7d276e000 100644 --- a/api/http/handler/endpoints/endpoint_registries_list.go +++ b/api/http/handler/endpoints/endpoint_registries_list.go @@ -27,14 +27,14 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } isAdmin := securityContext.IsAdmin @@ -92,7 +92,7 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name accessPolicies, err := kcl.GetNamespaceAccessPolicies() if err != nil { - return false, errors.Wrap(err, "unable to retrieve endpoint's namespaces policies") + return false, errors.Wrap(err, "unable to retrieve environment's namespaces policies") } namespacePolicy, ok := accessPolicies[namespace] diff --git a/api/http/handler/endpoints/endpoint_registry_access.go b/api/http/handler/endpoints/endpoint_registry_access.go index 8dde41d35..d74fe721e 100644 --- a/api/http/handler/endpoints/endpoint_registry_access.go +++ b/api/http/handler/endpoints/endpoint_registry_access.go @@ -25,7 +25,7 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error { func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err} } registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId") @@ -35,9 +35,9 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err} } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err} } securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -47,7 +47,7 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if !securityContext.IsAdmin { @@ -56,9 +56,9 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err} } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err} } var payload registryAccessPayload diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index 5cccf5aed..116868a6e 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -53,7 +53,7 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error { func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } var payload endpointSettingsUpdatePayload @@ -64,9 +64,9 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } securitySettings := endpoint.SecuritySettings @@ -111,7 +111,7 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting environment in database", err} } return response.JSON(w, endpoint) diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 4fc16acbe..67930cfe3 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -26,25 +26,25 @@ import ( func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if !snapshot.SupportDirectSnapshot(endpoint) { - return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this endpoint", err} + return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this environment", err} } snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } latestEndpointReference.Status = portainer.EndpointStatusUp @@ -57,7 +57,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } return response.Empty(w) diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 3c986a1ec..7ccf379ff 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -22,7 +22,7 @@ import ( func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } for _, endpoint := range endpoints { @@ -34,13 +34,13 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { - log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + log.Printf("background schedule error (environment snapshot). Environment not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) continue } endpoint.Status = portainer.EndpointStatusUp if snapshotError != nil { - log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) + log.Printf("background schedule error (environment snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) endpoint.Status = portainer.EndpointStatusDown } @@ -49,7 +49,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } } diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index 3577e3aac..390517251 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -65,19 +65,19 @@ type endpointStatusInspectResponse struct { func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if endpoint.EdgeID == "" { @@ -107,7 +107,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err} } settings, err := handler.DataStore.Settings().Settings() diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 6788867f1..91edd1d2c 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -71,7 +71,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error { func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } var payload endpointUpdatePayload @@ -82,9 +82,9 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if payload.Name != nil { @@ -257,7 +257,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment { _, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the environment", err} } } @@ -272,18 +272,18 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err} } if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation inside the database", err} } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint group inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment group inside the database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -307,7 +307,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation changes inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation changes inside the database", err} } } diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 015b063c9..f0a83ae6c 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -56,12 +56,12 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) { endpoint, err := middlewares.FetchEndpoint(request) if err != nil { - httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an endpoint on request context", err) + httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err) return } if !endpointutils.IsKubernetesEndpoint(endpoint) { - errMessage := "Endpoint is not a kubernetes endpoint" + errMessage := "Environment is not a kubernetes environment" httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage)) return } diff --git a/api/http/handler/kubernetes/kubernetes_config.go b/api/http/handler/kubernetes/kubernetes_config.go index 31734dcb1..898aef0a4 100644 --- a/api/http/handler/kubernetes/kubernetes_config.go +++ b/api/http/handler/kubernetes/kubernetes_config.go @@ -33,19 +33,19 @@ import ( func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } tokenData, err := security.RetrieveTokenData(r) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData) diff --git a/api/http/handler/kubernetes/kubernetes_nodes_limits.go b/api/http/handler/kubernetes/kubernetes_nodes_limits.go index 5e1f06215..18a46d05c 100644 --- a/api/http/handler/kubernetes/kubernetes_nodes_limits.go +++ b/api/http/handler/kubernetes/kubernetes_nodes_limits.go @@ -28,14 +28,14 @@ import ( func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} } endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint) diff --git a/api/http/handler/kubernetes/namespaces_toggle_system.go b/api/http/handler/kubernetes/namespaces_toggle_system.go index 5c71bf748..8caad850f 100644 --- a/api/http/handler/kubernetes/namespaces_toggle_system.go +++ b/api/http/handler/kubernetes/namespaces_toggle_system.go @@ -36,7 +36,7 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error { func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { endpoint, err := middlewares.FetchEndpoint(r) if err != nil { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment on request context", err} } namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace") diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index fe5a0526f..64132b617 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -71,9 +71,9 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers { @@ -96,7 +96,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } tokenData, err := security.RetrieveTokenData(r) diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 424255b89..d2038459d 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -72,9 +72,9 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) @@ -85,7 +85,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt if !isOrphaned { err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { @@ -149,14 +149,14 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } stack = &portainer.Stack{ diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index 35d38d378..e5c5056d3 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -54,16 +54,16 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { if !securityContext.IsAdmin { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if endpoint != nil { err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index 05c1ab102..b1cb8638b 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -49,16 +49,16 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { if !securityContext.IsAdmin { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } if endpoint != nil { err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index f4b4fb673..aeb950bda 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -42,7 +42,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} } stacks, err := handler.DataStore.Stack().Stacks() diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 19377f418..cfca05944 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -26,7 +26,7 @@ type stackMigratePayload struct { func (payload *stackMigratePayload) Validate(r *http.Request) error { if payload.EndpointID == 0 { - return errors.New("Invalid endpoint identifier. Must be a positive number") + return errors.New("Invalid environment identifier. Must be a positive number") } return nil } @@ -68,14 +68,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { @@ -111,9 +111,9 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } stack.EndpointID = portainer.EndpointID(payload.EndpointID) @@ -132,7 +132,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht } if !isUnique { - errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name) + errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on environment '%s'", stack.Name, targetEndpoint.Name) return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} } diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index 4b036a287..3a6ca0285 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -50,14 +50,14 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "") diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index 8b435c560..6aea8e375 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -48,14 +48,14 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 99aeab570..49476fe7a 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -91,14 +91,14 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err} } securityContext, err := security.RetrieveRestrictedRequestContext(r) diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index 7e1e7883c..48b3b14d2 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -88,14 +88,14 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) * endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err} } if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index 445ba9604..9b9d485ee 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -84,14 +84,14 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the environment associated to the stack inside the database", Err: err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err} + return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err} } securityContext, err := security.RetrieveRestrictedRequestContext(r) diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index f1465dfce..9d756cb89 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -43,7 +43,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe for endpointID := range tag.Endpoints { endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err} } tagIdx := findTagIndex(endpoint.TagIDs, tagID) @@ -51,7 +51,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx) err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment", err} } } } @@ -59,7 +59,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe for endpointGroupID := range tag.EndpointGroups { endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpointGroupID) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint group from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment group from the database", err} } tagIdx := findTagIndex(endpointGroup.TagIDs, tagID) @@ -67,14 +67,14 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx) err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment group", err} } } } endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} } edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() @@ -91,7 +91,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) { err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint relations in the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment relations in the database", err} } } } @@ -103,7 +103,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe edgeGroup.TagIDs = removeElement(edgeGroup.TagIDs, tagIdx) err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment group", err} } } } diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index 5449bd07a..2fbc12772 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -46,9 +46,9 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} } imageTag, _ := request.RetrieveQueryParameter(r, "tag", true) diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index 7b28ec655..ef7b13a8d 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -48,14 +48,14 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request) endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } params := &webSocketRequestParams{ diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 7e451b252..149a15a15 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -55,14 +55,14 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == errors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } params := &webSocketRequestParams{ diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go index e917d5a75..bd3dd0871 100644 --- a/api/http/handler/websocket/pod.go +++ b/api/http/handler/websocket/pod.go @@ -64,14 +64,14 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } serviceAccountToken, isAdminToken, err := handler.getToken(r, endpoint, false) diff --git a/api/http/handler/websocket/shell_pod.go b/api/http/handler/websocket/shell_pod.go index e7e5861b3..a2bb727a0 100644 --- a/api/http/handler/websocket/shell_pod.go +++ b/api/http/handler/websocket/shell_pod.go @@ -25,14 +25,14 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err} } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err} } tokenData, err := security.RetrieveTokenData(r) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) diff --git a/api/http/middlewares/endpoint.go b/api/http/middlewares/endpoint.go index b23d3ac5a..2f3967e0c 100644 --- a/api/http/middlewares/endpoint.go +++ b/api/http/middlewares/endpoint.go @@ -25,7 +25,7 @@ func WithEndpoint(endpointService portainer.EndpointService, endpointIDParam str endpointID, err := requesthelpers.RetrieveNumericRouteVariableValue(request, endpointIDParam) if err != nil { - httperror.WriteError(rw, http.StatusBadRequest, "Invalid endpoint identifier route variable", err) + httperror.WriteError(rw, http.StatusBadRequest, "Invalid environment identifier route variable", err) return } @@ -36,7 +36,7 @@ func WithEndpoint(endpointService portainer.EndpointService, endpointIDParam str if err == bolterrors.ErrObjectNotFound { statusCode = http.StatusNotFound } - httperror.WriteError(rw, statusCode, "Unable to find an endpoint with the specified identifier inside the database", err) + httperror.WriteError(rw, statusCode, "Unable to find an environment with the specified identifier inside the database", err) return } @@ -51,7 +51,7 @@ func WithEndpoint(endpointService portainer.EndpointService, endpointIDParam str func FetchEndpoint(request *http.Request) (*portainer.Endpoint, error) { contextData := request.Context().Value(contextEndpoint) if contextData == nil { - return nil, errors.New("Unable to find endpoint data in request context") + return nil, errors.New("Unable to find environment data in request context") } return contextData.(*portainer.Endpoint), nil diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index dee955e50..692c49544 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -113,7 +113,7 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp // AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge endpoint func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error { if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { - return errors.New("Invalid endpoint type") + return errors.New("Invalid environment type") } edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader) diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go index 249ed168c..2640958bb 100644 --- a/api/internal/snapshot/snapshot.go +++ b/api/internal/snapshot/snapshot.go @@ -126,7 +126,7 @@ func (service *Service) startSnapshotLoop() error { go func() { err := service.snapshotEndpoints() if err != nil { - log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (endpoint snapshot).] [error: %s]", err) + log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err) } for { @@ -134,7 +134,7 @@ func (service *Service) startSnapshotLoop() error { case <-ticker.C: err := service.snapshotEndpoints() if err != nil { - log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (endpoint snapshot).] [error: %s]", err) + log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err) } case <-service.shutdownCtx.Done(): log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]") @@ -166,13 +166,13 @@ func (service *Service) snapshotEndpoints() error { latestEndpointReference, err := service.dataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { - log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + log.Printf("background schedule error (environment snapshot). Environment not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) continue } latestEndpointReference.Status = portainer.EndpointStatusUp if snapshotError != nil { - log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) + log.Printf("background schedule error (environment snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) latestEndpointReference.Status = portainer.EndpointStatusDown } @@ -181,7 +181,7 @@ func (service *Service) snapshotEndpoints() error { err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { - log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + log.Printf("background schedule error (environment snapshot). Unable to update environment (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) continue } } @@ -201,12 +201,12 @@ func FetchDockerID(snapshot portainer.DockerSnapshot) (string, error) { } if info["Swarm"] == nil { - return "", errors.New("swarm endpoint is missing swarm info snapshot") + return "", errors.New("swarm environment is missing swarm info snapshot") } swarmInfo := info["Swarm"].(map[string]interface{}) if swarmInfo["Cluster"] == nil { - return "", errors.New("swarm endpoint is missing cluster info snapshot") + return "", errors.New("swarm environment is missing cluster info snapshot") } clusterInfo := swarmInfo["Cluster"].(map[string]interface{}) diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index 0fc40d384..85344ca36 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -94,7 +94,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kuber return factory.buildEdgeClient(endpoint) } - return nil, errors.New("unsupported endpoint type") + return nil, errors.New("unsupported environment type") } type agentHeaderRoundTripper struct { @@ -125,7 +125,7 @@ func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*ku if tunnel.Status == portainer.EdgeAgentIdle { err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID) if err != nil { - return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err) + return nil, fmt.Errorf("failed opening tunnel to environment: %w", err) } if endpoint.EdgeCheckinInterval == 0 { diff --git a/api/stacks/deploy.go b/api/stacks/deploy.go index ccf5eb441..797a415e3 100644 --- a/api/stacks/deploy.go +++ b/api/stacks/deploy.go @@ -51,7 +51,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data endpoint, err := datastore.Endpoint().Endpoint(stack.EndpointID) if err != nil { - return errors.WithMessagef(err, "failed to find the endpoint %v associated to the stack %v", stack.EndpointID, stack.ID) + return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID) } author := stack.UpdatedBy diff --git a/api/stacks/deploy_test.go b/api/stacks/deploy_test.go index 58b7de913..6ccb9beea 100644 --- a/api/stacks/deploy_test.go +++ b/api/stacks/deploy_test.go @@ -101,7 +101,7 @@ func Test_redeployWhenChanged(t *testing.T) { tmpDir, _ := ioutil.TempDir("", "stack") err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1}) - assert.NoError(t, err, "error creating endpoint") + assert.NoError(t, err, "error creating environment") username := "user" err = store.User().CreateUser(&portainer.User{Username: username, Role: portainer.AdministratorRole}) diff --git a/app/azure/_module.js b/app/azure/_module.js index 30fd789b9..10c6cf60c 100644 --- a/app/azure/_module.js +++ b/app/azure/_module.js @@ -20,7 +20,7 @@ angular.module('portainer.azure', ['portainer.app']).config([ EndpointProvider.setOfflineModeFromStatus(endpoint.Status); await StateManager.updateEndpointState(endpoint, []); } catch (e) { - Notifications.error('Failed loading endpoint', e); + Notifications.error('Failed loading environment', e); $state.go('portainer.home', {}, { reload: true }); } }); diff --git a/app/docker/__module.js b/app/docker/__module.js index 94e7e969c..a34a8e4cd 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -24,10 +24,10 @@ angular.module('portainer.docker', ['portainer.app']).config([ if (status === 2) { if (!endpoint.Snapshots[0]) { - throw new Error('Endpoint is unreachable and there is no snapshot available for offline browsing.'); + throw new Error('Environment is unreachable and there is no snapshot available for offline browsing.'); } if (endpoint.Snapshots[0].Swarm) { - throw new Error('Endpoint is unreachable. Connect to another swarm manager.'); + throw new Error('Environment is unreachable. Connect to another swarm manager.'); } } @@ -38,7 +38,7 @@ angular.module('portainer.docker', ['portainer.app']).config([ const extensions = await LegacyExtensionManager.initEndpointExtensions(endpoint); await StateManager.updateEndpointState(endpoint, extensions); } catch (e) { - Notifications.error('Failed loading endpoint', e); + Notifications.error('Failed loading environment', e); $state.go('portainer.home', {}, { reload: true }); } diff --git a/app/docker/views/dashboard/dashboard.html b/app/docker/views/dashboard/dashboard.html index 2e909e6bc..cdc2ba083 100644 --- a/app/docker/views/dashboard/dashboard.html +++ b/app/docker/views/dashboard/dashboard.html @@ -1,6 +1,6 @@ - Endpoint summary + Environment summary
@@ -34,12 +34,12 @@
- + - + - +
EndpointEnvironment {{ endpoint.Name }} diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.html b/app/docker/views/docker-features-configuration/docker-features-configuration.html index d906d86c6..3ca6b421f 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.html +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -14,7 +14,7 @@
- These features are only available for an Agent enabled endpoints. + These features are only available for an Agent enabled environments.
diff --git a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.html b/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.html index d19f4b8bc..f5da826c8 100644 --- a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.html +++ b/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.html @@ -52,7 +52,7 @@
Loading...
No endpoint available.No environment available.
diff --git a/app/edge/components/edge-job-form/edgeJobForm.html b/app/edge/components/edge-job-form/edgeJobForm.html index d94f24004..b3d75e3a2 100644 --- a/app/edge/components/edge-job-form/edgeJobForm.html +++ b/app/edge/components/edge-job-form/edgeJobForm.html @@ -75,7 +75,7 @@
- Time should be set according to the chosen endpoints' timezone. + Time should be set according to the chosen environments' timezone.
@@ -130,7 +130,7 @@ />
- Time should be set according to the chosen endpoints' timezone. + Time should be set according to the chosen environments' timezone.
@@ -215,7 +215,7 @@
- Target endpoints + Target environments
- Endpoint + Environment diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html index 69bb34b98..2cb16bc40 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html +++ b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.html @@ -57,7 +57,7 @@ Loading... - No endpoint available. + No environment available. diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js index 8a0c12765..f71f6d330 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js +++ b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js @@ -101,7 +101,7 @@ export class EdgeStackEndpointsDatatableController { this.state.filteredDataSet = endpoints; this.state.totalFilteredDataSet = totalCount; } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + this.Notifications.error('Failure', err, 'Unable to retrieve environments'); } finally { this.state.loading = false; } diff --git a/app/edge/components/edge-stack-status/edgeStackStatus.html b/app/edge/components/edge-stack-status/edgeStackStatus.html index 5ffda22b5..02c8a07d0 100644 --- a/app/edge/components/edge-stack-status/edgeStackStatus.html +++ b/app/edge/components/edge-stack-status/edgeStackStatus.html @@ -1,3 +1,3 @@ -{{ $ctrl.status.acknowledged || 0 }} -{{ $ctrl.status.ok || 0 }} -{{ $ctrl.status.error || 0 }} +{{ $ctrl.status.acknowledged || 0 }} +{{ $ctrl.status.ok || 0 }} +{{ $ctrl.status.error || 0 }} diff --git a/app/edge/components/group-form/groupForm.html b/app/edge/components/group-form/groupForm.html index 128b9e6e7..2d9c79dc4 100644 --- a/app/edge/components/group-form/groupForm.html +++ b/app/edge/components/group-form/groupForm.html @@ -30,7 +30,7 @@ Static
-

Manually select Edge endpoints

+

Manually select Edge environments

@@ -40,7 +40,7 @@ Dynamic
-

Automatically associate endpoints via tags

+

Automatically associate environments via tags

@@ -49,9 +49,9 @@
- +
- Associated endpoints + Associated environments
-
No Edge endpoints are available. Head over to the Endpoints view to add endpoints.
+
No Edge environments are available. Head over to the Environments view to add environments.
@@ -84,7 +84,7 @@ Partial match
-

Associate any endpoint matching at least one of the selected tags

+

Associate any environment matching at least one of the selected tags

@@ -94,7 +94,7 @@ Full match
-

Associate any endpoint matching all of the selected tags

+

Associate any environment matching all of the selected tags

@@ -106,7 +106,7 @@
- Associated endpoints by tags + Associated environments by tags
- Endpoints Count + Environments Count diff --git a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js index f88e9576d..190f45fb4 100644 --- a/app/edge/views/edge-jobs/edgeJob/edgeJobController.js +++ b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js @@ -161,7 +161,7 @@ export class EdgeJobController { this.results = results; } } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); + this.Notifications.error('Failure', err, 'Unable to retrieve environment list'); } this.$window.onbeforeunload = () => { diff --git a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html index 74bab4ec9..d5d5c04b5 100644 --- a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html +++ b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html @@ -26,11 +26,11 @@
- Endpoints + Environments
KubernetesEndpointConverter.apiToEndpoint(item)); } catch (err) { - throw new PortainerError('Unable to retrieve endpoints', err); + throw new PortainerError('Unable to retrieve environments', err); } } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index a8b3304ee..149b90152 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -926,7 +926,7 @@

Server metrics features must be enabled in the - endpoint configuration view. + environment configuration view.

diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js index 1c695d042..2d649df18 100644 --- a/app/kubernetes/views/cluster/clusterController.js +++ b/app/kubernetes/views/cluster/clusterController.js @@ -66,7 +66,7 @@ class KubernetesClusterController { }); } } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + this.Notifications.error('Failure', err, 'Unable to retrieve environments'); } } diff --git a/app/kubernetes/views/cluster/node/nodeController.js b/app/kubernetes/views/cluster/node/nodeController.js index bb5c3a90f..9a726add7 100644 --- a/app/kubernetes/views/cluster/node/nodeController.js +++ b/app/kubernetes/views/cluster/node/nodeController.js @@ -238,7 +238,7 @@ class KubernetesNodeController { }); } } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + this.Notifications.error('Failure', err, 'Unable to retrieve environments'); } } diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 9ea2f7cf5..4f9dc6d70 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -1,5 +1,5 @@ - Endpoints > {{ ctrl.endpoint.Name }} > Kubernetes configuration + Environment > {{ ctrl.endpoint.Name }} > Kubernetes configuration diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 8e41a3848..4e38cd27d 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -294,7 +294,7 @@ class KubernetesConfigureController { this.oldFormValues = Object.assign({}, this.formValues); } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration'); + this.Notifications.error('Failure', err, 'Unable to retrieve environment configuration'); } finally { this.state.viewReady = true; } diff --git a/app/kubernetes/views/dashboard/dashboard.html b/app/kubernetes/views/dashboard/dashboard.html index e449d57f3..903c54075 100644 --- a/app/kubernetes/views/dashboard/dashboard.html +++ b/app/kubernetes/views/dashboard/dashboard.html @@ -1,5 +1,5 @@ - Endpoint summary + Environment summary @@ -8,12 +8,12 @@
- + - + diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html index 7a34c74c0..d6ba1d52b 100644 --- a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html @@ -38,8 +38,8 @@
- No user nor team access has been set on the endpoint. Head over to the - endpoint access view to manage them. + No user nor team access has been set on the environment. Head over to the + environment access view to manage them.
The ingress feature must be enabled in the - endpoint configuration view to be able to register ingresses inside this + environment configuration view to be able to register ingresses inside this namespace.
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index 90b33db11..29d39b3a4 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -165,7 +165,7 @@
The ingress feature must be enabled in the - endpoint configuration view to be able to register ingresses inside + environment configuration view to be able to register ingresses inside this namespace.
diff --git a/app/portainer/__module.js b/app/portainer/__module.js index d5a29f75e..a2432ba9b 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -72,7 +72,7 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo return endpoint; } catch (e) { - Notifications.error('Failed loading endpoint', e); + Notifications.error('Failed loading environment', e); $state.go('portainer.home', {}, { reload: true }); return; } diff --git a/app/portainer/components/access-datatable/accessDatatable.html b/app/portainer/components/access-datatable/accessDatatable.html index 7dc4540d3..52c323103 100644 --- a/app/portainer/components/access-datatable/accessDatatable.html +++ b/app/portainer/components/access-datatable/accessDatatable.html @@ -3,7 +3,7 @@
- Access tagged as inherited are inherited from the group access. They cannot be removed or modified at the endpoint level but they can be overriden. + Access tagged as inherited are inherited from the group access. They cannot be removed or modified at the environment level but they can be overriden.
Access tagged as override are overriding the group access for the related users/teams.
diff --git a/app/portainer/components/accessControlPanel/porAccessControlPanel.html b/app/portainer/components/accessControlPanel/porAccessControlPanel.html index 40e6135f0..5fc0967b3 100644 --- a/app/portainer/components/accessControlPanel/porAccessControlPanel.html +++ b/app/portainer/components/accessControlPanel/porAccessControlPanel.html @@ -18,7 +18,7 @@ {{ $ctrl.resourceControl.Ownership }} @@ -145,7 +145,7 @@ Public
-

I want any user with access to this endpoint to be able to manage this resource

+

I want any user with access to this environment to be able to manage this resource

diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html index 26136e419..5cd20d771 100644 --- a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html @@ -1,11 +1,11 @@
- You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click on any endpoint entry to move it from one table to the + You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one table to the other.
-
Available endpoints
+
Available environments
-
Associated endpoints
+
Associated environments
- +
EndpointEnvironment {{ ctrl.endpoint.Name }}
- Endpoint + Environment Role diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index 415191f4b..54023c703 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -15,7 +15,7 @@ Remove Loading...
No endpoint available.No environment available.
diff --git a/app/portainer/components/datatables/roles-datatable/rolesDatatable.html b/app/portainer/components/datatables/roles-datatable/rolesDatatable.html index 107e08fb6..9ea5b1500 100644 --- a/app/portainer/components/datatables/roles-datatable/rolesDatatable.html +++ b/app/portainer/components/datatables/roles-datatable/rolesDatatable.html @@ -22,20 +22,20 @@ - Endpoint administrator - Full control of all resources in an endpoint + Environment administrator + Full control of all resources in an environment Helpdesk - Read-only access of all resources in an endpoint + Read-only access of all resources in an environment Read-only user - Read-only access of assigned resources in an endpoint + Read-only access of assigned resources in an environment Standard user - Full control of assigned resources in an endpoint + Full control of assigned resources in an environment diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index 8d440d045..552b0ee23 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -180,7 +180,7 @@ tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" - uib-tooltip="This stack was created inside an endpoint that is no longer registered inside Portainer." + uib-tooltip="This stack was created inside an environment that is no longer registered inside Portainer." > Orphaned diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index f24929b28..cd2b2a5a8 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -49,7 +49,7 @@ Loading...
- No endpoint available. + No environment available.
diff --git a/app/portainer/components/endpoint-selector/endpointSelector.html b/app/portainer/components/endpoint-selector/endpointSelector.html index c3800195f..10b057c56 100644 --- a/app/portainer/components/endpoint-selector/endpointSelector.html +++ b/app/portainer/components/endpoint-selector/endpointSelector.html @@ -1,5 +1,5 @@ - + {{ $select.selected.Name }} diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurity.html b/app/portainer/components/endpointSecurity/porEndpointSecurity.html index f9c7fee8f..c18c64fa1 100644 --- a/app/portainer/components/endpointSecurity/porEndpointSecurity.html +++ b/app/portainer/components/endpointSecurity/porEndpointSecurity.html @@ -4,7 +4,7 @@
diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index 6b0b81cb4..16de62842 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -36,20 +36,20 @@ > - +
- Associated endpoints + Associated environments
- You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click on any endpoint entry to move it from one table to + You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one table to the other.
-
Available endpoints
+
Available environments
-
Associated endpoints
+
Associated environments
@@ -87,7 +87,7 @@
- Unassociated endpoints + Unassociated environments
diff --git a/app/portainer/components/forms/group-form/groupFormController.js b/app/portainer/components/forms/group-form/groupFormController.js index 1f2c7b54d..9f9eb7acf 100644 --- a/app/portainer/components/forms/group-form/groupFormController.js +++ b/app/portainer/components/forms/group-form/groupFormController.js @@ -38,10 +38,10 @@ class GroupFormController { } else if (this.pageType === 'edit') { this.GroupService.addEndpoint(this.model.Id, endpoint) .then(() => { - this.Notifications.success('Success', 'Endpoint successfully added to group'); + this.Notifications.success('Success', 'Environment successfully added to group'); this.reloadTablesContent(); }) - .catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group')); + .catch((err) => this.Notifications.error('Error', err, 'Unable to add environment to group')); } } @@ -51,10 +51,10 @@ class GroupFormController { } else if (this.pageType === 'edit') { this.GroupService.removeEndpoint(this.model.Id, endpoint.Id) .then(() => { - this.Notifications.success('Success', 'Endpoint successfully removed from group'); + this.Notifications.success('Success', 'Environment successfully removed from group'); this.reloadTablesContent(); }) - .catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group')); + .catch((err) => this.Notifications.error('Error', err, 'Unable to remove environment from group')); } } diff --git a/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html b/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html index 1f3907ee4..30c499458 100644 --- a/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html +++ b/app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html @@ -65,7 +65,7 @@
{{ $ctrl.state.formValidationError }}
This template type cannot be deployed on this endpoint.
This template type cannot be deployed on this environment.
diff --git a/app/portainer/components/information-panel-offline/informationPanelOffline.html b/app/portainer/components/information-panel-offline/informationPanelOffline.html index 17942ee5a..bcd5bfc51 100644 --- a/app/portainer/components/information-panel-offline/informationPanelOffline.html +++ b/app/portainer/components/information-panel-offline/informationPanelOffline.html @@ -2,7 +2,7 @@

- This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot. + This environment is currently offline (read-only). Data shown is based on the latest available snapshot.

diff --git a/app/portainer/components/information-panel-offline/informationPanelOfflineController.js b/app/portainer/components/information-panel-offline/informationPanelOfflineController.js index d4096c1c5..14329ca72 100644 --- a/app/portainer/components/information-panel-offline/informationPanelOfflineController.js +++ b/app/portainer/components/information-panel-offline/informationPanelOfflineController.js @@ -18,7 +18,7 @@ angular.module('portainer.app').controller('InformationPanelOfflineController', $state.reload(); }) .catch(function onError(err) { - Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); + Notifications.error('Failure', err, 'An error occured during environment snapshot'); }); } @@ -31,7 +31,7 @@ angular.module('portainer.app').controller('InformationPanelOfflineController', ctrl.snapshotTime = data.Snapshots[0].Time; }) .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + Notifications.error('Failure', err, 'Unable to retrieve environment information'); }); } }, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 852d398f9..827b40b60 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -57,7 +57,7 @@ angular.module('portainer.app').factory('EndpointService', [ }) .catch(function error(err) { deferred.notify({ upload: false }); - deferred.reject({ msg: 'Unable to update endpoint', err: err }); + deferred.reject({ msg: 'Unable to update environment', err: err }); }); return deferred.promise; }; @@ -84,7 +84,7 @@ angular.module('portainer.app').factory('EndpointService', [ deferred.resolve(response.data); }) .catch(function error(err) { - deferred.reject({ msg: 'Unable to create endpoint', err: err }); + deferred.reject({ msg: 'Unable to create environment', err: err }); }); return deferred.promise; @@ -131,7 +131,7 @@ angular.module('portainer.app').factory('EndpointService', [ deferred.resolve(response.data); }) .catch(function error(err) { - deferred.reject({ msg: 'Unable to create endpoint', err: err }); + deferred.reject({ msg: 'Unable to create environment', err: err }); }); return deferred.promise; @@ -145,7 +145,7 @@ angular.module('portainer.app').factory('EndpointService', [ deferred.resolve(response.data); }) .catch(function error(err) { - deferred.reject({ msg: 'Unable to create endpoint', err: err }); + deferred.reject({ msg: 'Unable to create environment', err: err }); }); return deferred.promise; diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index da3a1a545..33ee75401 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -56,7 +56,7 @@ angular.module('portainer.app').factory('StackService', [ .then(function success(data) { var swarm = data; if (swarm.Id === stack.SwarmId) { - deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null }); + deferred.reject({ msg: 'Target environment is located in the same Swarm cluster as the current environment', err: null }); return; } return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise; diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 7e03ba7bb..9203d6248 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -154,9 +154,9 @@ angular.module('portainer.app').factory('ModalService', [ service.confirmDeassociate = function (callback) { const message = - '

De-associating this Edge endpoint will mark it as non associated and will clear the registered Edge ID.

' + - '

Any agent started with the Edge key associated to this endpoint will be able to re-associate with this endpoint.

' + - '

You can re-use the Edge ID and Edge key that you used to deploy the existing Edge agent to associate a new Edge device to this endpoint.

'; + '

De-associating this Edge environment will mark it as non associated and will clear the registered Edge ID.

' + + '

Any agent started with the Edge key associated to this environment will be able to re-associate with this environment.

' + + '

You can re-use the Edge ID and Edge key that you used to deploy the existing Edge agent to associate a new Edge device to this environment.

'; service.confirm({ title: 'About de-associating', message: $sanitize(message), @@ -258,7 +258,7 @@ angular.module('portainer.app').factory('ModalService', [ service.confirmEndpointSnapshot = function (callback) { service.confirm({ title: 'Are you sure?', - message: 'Triggering a manual refresh will poll each endpoint to retrieve its information, this may take a few moments.', + message: 'Triggering a manual refresh will poll each environment to retrieve its information, this may take a few moments.', buttons: { confirm: { label: 'Continue', diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 047a29813..5930e82c9 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -200,7 +200,7 @@ angular.module('portainer.app').factory('StateManager', [ deferred.resolve(); }) .catch(function error(err) { - deferred.reject({ msg: 'Unable to connect to the Docker endpoint', err: err }); + deferred.reject({ msg: 'Unable to connect to the Docker environment', err: err }); }) .finally(function final() { state.loading = false; diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html index 84c52b69d..222f62f4e 100644 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html +++ b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html @@ -7,7 +7,7 @@

- Forcing HTTPs only will cause Portainer to stop listening on the HTTP port. Any edge agent endpoint that is using HTTP will no longer be available. + Forcing HTTPs only will cause Portainer to stop listening on the HTTP port. Any edge agent environment that is using HTTP will no longer be available.

diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index e132f7dae..1735c66bd 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -129,7 +129,7 @@ class AuthenticationController { return this.$state.go('portainer.home'); } } catch (err) { - this.error(err, 'Unable to retrieve endpoints'); + this.error(err, 'Unable to retrieve environments'); } } diff --git a/app/portainer/views/endpoints/access/endpointAccess.html b/app/portainer/views/endpoints/access/endpointAccess.html index ea754aac7..f0c96ea86 100644 --- a/app/portainer/views/endpoints/access/endpointAccess.html +++ b/app/portainer/views/endpoints/access/endpointAccess.html @@ -1,14 +1,14 @@ - + - Endpoints > {{ ctrl.endpoint.Name }} > Access management + Environments > {{ ctrl.endpoint.Name }} > Access management
- + diff --git a/app/portainer/views/endpoints/access/endpointAccessController.js b/app/portainer/views/endpoints/access/endpointAccessController.js index e16b56442..6d7e2329e 100644 --- a/app/portainer/views/endpoints/access/endpointAccessController.js +++ b/app/portainer/views/endpoints/access/endpointAccessController.js @@ -20,7 +20,7 @@ class EndpointAccessController { this.endpoint = await this.EndpointService.endpoint(this.$transition$.params().id); this.group = await this.GroupService.group(this.endpoint.GroupId); } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + this.Notifications.error('Failure', err, 'Unable to retrieve environment information'); } } diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index dde612ab5..828ecbae9 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -112,11 +112,11 @@ angular $scope.state.actionInProgress = true; EndpointService.createLocalEndpoint(name, URL, publicURL, groupId, tagIds) .then(function success() { - Notifications.success('Endpoint created', name); + Notifications.success('Environment created', name); $state.go('portainer.endpoints', {}, { reload: true }); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); + Notifications.error('Failure', err, 'Unable to create environment'); }) .finally(function final() { $scope.state.actionInProgress = false; @@ -158,11 +158,11 @@ angular $scope.state.actionInProgress = true; EndpointService.createLocalKubernetesEndpoint(name, tagIds) .then(function success(result) { - Notifications.success('Endpoint created', name); + Notifications.success('Environment created', name); $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: result.Id }); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); + Notifications.error('Failure', err, 'Unable to create environment'); }) .finally(function final() { $scope.state.actionInProgress = false; @@ -220,11 +220,11 @@ angular $scope.state.actionInProgress = true; EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) .then(function success() { - Notifications.success('Endpoint created', name); + Notifications.success('Environment created', name); $state.go('portainer.endpoints', {}, { reload: true }); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); + Notifications.error('Failure', err, 'Unable to create environment'); }) .finally(function final() { $scope.state.actionInProgress = false; @@ -251,7 +251,7 @@ angular CheckinInterval ); - Notifications.success('Endpoint created', name); + Notifications.success('Environment created', name); switch (endpoint.Type) { case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment: case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment: @@ -267,7 +267,7 @@ angular return endpoint; } catch (err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); + Notifications.error('Failure', err, 'Unable to create environment'); } finally { $scope.state.actionInProgress = false; } diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 3827ff671..a3c32cab8 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -1,6 +1,6 @@ - - Endpoints > Add endpoint + + Environments > Add environment
@@ -130,8 +130,8 @@

- Allows you to create an endpoint that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the - required information on how to connect an Edge agent to this endpoint will be available after endpoint creation. + Allows you to create an environment that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the + required information on how to connect an Edge agent to this environment will be available after environment creation.

You can read more about the Edge agent in the userguide available here.

@@ -257,7 +257,7 @@
@@ -350,7 +350,7 @@ Public IP @@ -487,8 +487,8 @@ analytics-event="portainer-endpoint-creation" analytics-properties="{ metadata: { type: 'docker-api' } }" > - Add endpoint - Creating endpoint... + Add environment + Creating environment...
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 1078a01e2..aeb80bd9b 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -1,11 +1,11 @@ - + - Endpoints > {{ endpoint.Name }} + Environments > {{ endpoint.Name }} @@ -14,7 +14,7 @@

- This Edge endpoint is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}. + This Edge environment is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}.

Edge key: {{ endpoint.EdgeKey }} @@ -86,7 +86,7 @@

- For those prestaging the edge agent, use the following join token to associate the Edge agent with this endpoint. + For those prestaging the edge agent, use the following join token to associate the Edge agent with this environment.

You can read more about pre-staging in the userguide available here.

@@ -128,7 +128,7 @@
@@ -166,7 +166,7 @@ Poll frequency
@@ -220,8 +220,8 @@ ng-click="updateEndpoint()" button-spinner="state.actionInProgress" > - Update endpoint - Updating endpoint... + Update environment + Updating environment... Cancel
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 2318a9509..f28cb28c9 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -125,10 +125,10 @@ function EndpointController( try { $scope.state.actionInProgress = true; await EndpointService.deassociateEndpoint(endpoint.Id); - Notifications.success('Endpoint de-associated', $scope.endpoint.Name); + Notifications.success('Environment de-associated', $scope.endpoint.Name); $state.reload(); } catch (err) { - Notifications.error('Failure', err, 'Unable to de-associate endpoint'); + Notifications.error('Failure', err, 'Unable to de-associate environment'); } finally { $scope.state.actionInProgress = false; } @@ -179,12 +179,12 @@ function EndpointController( $scope.state.actionInProgress = true; EndpointService.updateEndpoint(endpoint.Id, payload).then( function success() { - Notifications.success('Endpoint updated', $scope.endpoint.Name); + Notifications.success('Environment updated', $scope.endpoint.Name); EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); $state.go('portainer.endpoints', {}, { reload: true }); }, function error(err) { - Notifications.error('Failure', err, 'Unable to update endpoint'); + Notifications.error('Failure', err, 'Unable to update environment'); $scope.state.actionInProgress = false; }, function update(evt) { @@ -264,7 +264,7 @@ function EndpointController( configureState(); } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); + Notifications.error('Failure', err, 'Unable to retrieve environment details'); } }); } diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 7f1813ca7..657cceb86 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -1,16 +1,16 @@ - + - Endpoint management + Environment management
- + Groups > {{ group.Name }} > Access management diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index bddbfb650..f19069355 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -1,6 +1,6 @@ - - Endpoint groups > Add group + + Environment groups > Add group
diff --git a/app/portainer/views/groups/edit/group.html b/app/portainer/views/groups/edit/group.html index 0c0566486..ddbb927d1 100644 --- a/app/portainer/views/groups/edit/group.html +++ b/app/portainer/views/groups/edit/group.html @@ -1,5 +1,5 @@ - + Groups > {{ ::group.Name }} diff --git a/app/portainer/views/groups/groups.html b/app/portainer/views/groups/groups.html index 42e491286..714af8c68 100644 --- a/app/portainer/views/groups/groups.html +++ b/app/portainer/views/groups/groups.html @@ -1,14 +1,14 @@ - + - Endpoint group management + Environment group management
- +
diff --git a/app/portainer/views/groups/groupsController.js b/app/portainer/views/groups/groupsController.js index 67fc81af6..b261968d4 100644 --- a/app/portainer/views/groups/groupsController.js +++ b/app/portainer/views/groups/groupsController.js @@ -15,7 +15,7 @@ function GroupsController($scope, $state, $async, GroupService, Notifications) { try { await GroupService.deleteGroup(group.Id); - Notifications.success('Endpoint group successfully removed', group.Name); + Notifications.success('Environment group successfully removed', group.Name); _.remove($scope.groups, group); } catch (err) { Notifications.error('Failure', err, 'Unable to remove group'); @@ -31,7 +31,7 @@ function GroupsController($scope, $state, $async, GroupService, Notifications) { $scope.groups = data; }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint groups'); + Notifications.error('Failure', err, 'Unable to retrieve environment groups'); $scope.groups = []; }); } diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 3fc6bd415..3b1a4d47c 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -4,7 +4,7 @@ - Endpoints + Environments @@ -26,7 +26,7 @@ style="width: 100%; height: 100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center;" ng-if="state.connectingToEdgeEndpoint" > - Connecting to the Edge endpoint... + Connecting to the Edge environment...
@@ -35,8 +35,8 @@

No environment available for management. Please head over the - endpoints view - to add an endpoint. + environments view + to add an environment.

@@ -44,7 +44,7 @@
- This will restore the Portainer metadata which contains information about the endpoints, stacks and applications, as well as the configured users. + This will restore the Portainer metadata which contains information about the environments, stacks and applications, as well as the configured users.
diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index 4d2686997..0db2685f9 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -8,10 +8,10 @@
- +
- +
@@ -22,7 +22,7 @@
- +
@@ -37,8 +37,8 @@
- - + +
@@ -48,7 +48,7 @@
- +
@@ -69,10 +69,10 @@
- +
- +
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index 235390a23..510982ac2 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -61,7 +61,7 @@ class InitEndpointController { case PortainerEndpointConnectionTypes.AGENT: return this.createAgentEndpoint(); default: - this.Notifications.error('Failure', 'Unable to determine which action to do to create endpoint'); + this.Notifications.error('Failure', 'Unable to determine which action to do to create environment'); } } diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index 742b3122f..3daf4862b 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -9,7 +9,7 @@ - View registries via an endpoint to manage access for user(s) and/or team(s) + View registries via an environment to manage access for user(s) and/or team(s) diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js index 568516de7..0fe991f30 100644 --- a/app/portainer/views/registries/registriesController.js +++ b/app/portainer/views/registries/registriesController.js @@ -23,7 +23,7 @@ angular.module('portainer.app').controller('RegistriesController', [ $scope.removeAction = function (selectedItems) { const regAttrMsg = selectedItems.length > 1 ? 'hese' : 'his'; const registriesMsg = selectedItems.length > 1 ? 'registries' : 'registry'; - const msg = `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more endpoints. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`; + const msg = `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more environments. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`; ModalService.confirmDeletion(msg, function onConfirm(confirmed) { if (!confirmed) { diff --git a/app/portainer/views/roles/roles.html b/app/portainer/views/roles/roles.html index a595909f1..9d73e610a 100644 --- a/app/portainer/views/roles/roles.html +++ b/app/portainer/views/roles/roles.html @@ -45,7 +45,7 @@
- Effective role for each endpoint will be displayed for the selected user + Effective role for each environment will be displayed for the selected user
diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index de3bbe41e..8b2245b3f 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -94,7 +94,7 @@ Edge agent default poll frequency
diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 5a365f0d1..fe83ac39b 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -80,7 +80,7 @@
This stack was created outside of Portainer. Control over this stack is limited. - This stack is orphaned. You can reassociate it with the current environment using the "Associate to this endpoint" feature. + This stack is orphaned. You can reassociate it with the current environment using the "Associate to this environment" feature.

@@ -82,10 +82,10 @@
- Associate to this endpoint + Associate to this environment

- This feature allows you to reassociate this stack to the current endpoint. + This feature allows you to reassociate this stack to the current environment.

diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 4c9289fed..f11546a34 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -70,9 +70,9 @@ angular.module('portainer.app').controller('StackController', [ } }; - $scope.$on('$destroy', function() { + $scope.$on('$destroy', function () { $scope.state.isEditorDirty = false; - }) + }); $scope.handleEnvVarChange = handleEnvVarChange; function handleEnvVarChange(value) { @@ -106,7 +106,7 @@ angular.module('portainer.app').controller('StackController', [ ModalService.confirm({ title: 'Are you sure?', message: - 'This action will deploy a new instance of this stack on the target endpoint, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.', + 'This action will deploy a new instance of this stack on the target environment, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.', buttons: { confirm: { label: 'Migrate', @@ -289,7 +289,7 @@ angular.module('portainer.app').controller('StackController', [ $scope.endpoints = data.value; }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + Notifications.error('Failure', err, 'Unable to retrieve environments'); }); $q.all({ diff --git a/app/portainer/views/users/edit/user.html b/app/portainer/views/users/edit/user.html index f8b339336..1dbc16fcf 100644 --- a/app/portainer/views/users/edit/user.html +++ b/app/portainer/views/users/edit/user.html @@ -24,7 +24,7 @@ Administrator diff --git a/app/portainer/views/users/users.html b/app/portainer/views/users/users.html index 7d888b46b..70ddb8ba4 100644 --- a/app/portainer/views/users/users.html +++ b/app/portainer/views/users/users.html @@ -75,7 +75,7 @@ Administrator @@ -112,8 +112,8 @@
- Note: non-administrator users who aren't in a team don't have access to any endpoints by default. Head over to the - Endpoints view to manage their accesses. + Note: non-administrator users who aren't in a team don't have access to any environments by default. Head over to the + Environments view to manage their accesses.
diff --git a/test/e2e/cypress/integration/ci_basic_tests/init.spec.js b/test/e2e/cypress/integration/ci_basic_tests/init.spec.js index b0440ea0a..a1cb21ff0 100644 --- a/test/e2e/cypress/integration/ci_basic_tests/init.spec.js +++ b/test/e2e/cypress/integration/ci_basic_tests/init.spec.js @@ -8,7 +8,7 @@ context('Init admin user test', () => { cy.url().should('include', 'init/endpoint'); cy.saveLocalStorage(); }); - it('Select local docker endpoint and init', function () { + it('Select local docker environment and init', function () { cy.initEndpoint(); cy.url().should('include', 'home'); }); diff --git a/test/e2e/cypress/integration/init.spec.js b/test/e2e/cypress/integration/init.spec.js index f1c69b4a0..7a0bce690 100644 --- a/test/e2e/cypress/integration/init.spec.js +++ b/test/e2e/cypress/integration/init.spec.js @@ -8,7 +8,7 @@ context('Init admin & local docker endpoint', () => { cy.url().should('include', 'init/endpoint'); cy.saveLocalStorage(); }); - it('Select local docker endpoint and init', function () { + it('Select local docker environment and init', function () { cy.initEndpoint(); cy.url().should('include', 'home'); }); From 79ca51c92e0587e45f3b5fcc5880f23c5baa73ca Mon Sep 17 00:00:00 2001 From: zees-dev <63374656+zees-dev@users.noreply.github.com> Date: Thu, 9 Sep 2021 15:23:10 +1200 Subject: [PATCH 03/26] - code cleanup by converting functions to error funcs (remove this bindings) (#5595) - remove redundant checked variable - detect readyState of websocket when closing to prevent redundant error --- .../kubectl-shell/kubectl-shell.controller.js | 42 +++++++++---------- .../kubectl-shell/kubectl-shell.html | 4 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js index 2c9423c7e..bf2bdde7c 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js @@ -13,13 +13,14 @@ export default class KubectlShellController { } disconnect() { - this.state.checked = false; - this.state.icon = 'fas fa-window-minimize'; - this.state.shell.socket.close(); - this.state.shell.term.dispose(); - this.state.shell.connected = false; - this.TerminalWindow.terminalclose(); - this.$window.onresize = null; + if (this.state.shell.connected) { + this.state.shell.connected = false; + this.state.icon = 'fas fa-window-minimize'; + this.state.shell.socket.close(); + this.state.shell.term.dispose(); + this.TerminalWindow.terminalclose(); + this.$window.onresize = null; + } } screenClear() { @@ -39,8 +40,7 @@ export default class KubectlShellController { } configureSocketAndTerminal(socket, term) { - var vm = this; - socket.onopen = function () { + socket.onopen = () => { const terminal_container = document.getElementById('terminal-container'); term.open(terminal_container); term.setOption('cursorBlink', true); @@ -51,31 +51,32 @@ export default class KubectlShellController { term.writeln(''); }; - term.on('data', function (data) { + term.on('data', (data) => { socket.send(data); }); - this.$window.onresize = function () { - vm.TerminalWindow.terminalresize(); - }; - - socket.onmessage = function (msg) { + socket.onmessage = (msg) => { term.write(msg.data); }; - socket.onerror = function (err) { + socket.onerror = (err) => { this.disconnect(); - this.Notifications.error('Failure', err, 'Websocket connection error'); - }.bind(this); + if (err.target.readyState !== WebSocket.CLOSED) { + this.Notifications.error('Failure', err, 'Websocket connection error'); + } + }; - this.state.shell.socket.onclose = this.disconnect.bind(this); + this.$window.onresize = () => { + this.TerminalWindow.terminalresize(); + }; + + socket.onclose = this.disconnect.bind(this); this.state.shell.connected = true; } connectConsole() { this.TerminalWindow.terminalopen(); - this.state.checked = true; this.state.css = 'normal'; const params = { @@ -101,7 +102,6 @@ export default class KubectlShellController { return this.$async(async () => { this.state = { css: 'normal', - checked: false, icon: 'fa-window-minimize', shell: { connected: false, diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.html b/app/kubernetes/components/kubectl-shell/kubectl-shell.html index a17c86533..2bd45a7fc 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.html +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.html @@ -13,7 +13,7 @@ -
+
kubectl shell
@@ -27,7 +27,7 @@
-
+
Loading Terminal...
From 5c8450c4c06d023bf61ccd71baac280ee6552582 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 9 Sep 2021 11:38:34 +0300 Subject: [PATCH 04/26] feat(edgestacks): support kubernetes edge stacks (#5276) [EE-393] --- api/http/handler/edgegroups/edgegroup_list.go | 37 ++- .../handler/edgegroups/edgegroup_list_test.go | 53 ++++ .../handler/edgestacks/edgestack_create.go | 235 +++++++++++++----- .../edgestacks/edgestack_create_test.go | 38 +++ .../handler/edgestacks/edgestack_delete.go | 17 +- api/http/handler/edgestacks/edgestack_file.go | 7 +- .../handler/edgestacks/edgestack_update.go | 90 ++++--- api/http/handler/edgestacks/endpoints.go | 60 +++++ api/http/handler/edgestacks/endpoints_test.go | 99 ++++++++ api/http/handler/edgestacks/handler.go | 46 +++- .../endpoint_edgestack_inspect.go | 27 +- api/http/server.go | 1 + api/internal/endpointutils/endpoint_test.go | 47 ++++ api/internal/testhelpers/datastore.go | 104 ++++++++ api/portainer.go | 31 ++- app/edge/__module.js | 4 +- .../edgeGroupsSelector.html | 10 +- .../components/edge-groups-selector/index.js | 3 +- ...ack-deployment-type-selector.controller.js | 21 ++ .../edge-stack-deployment-type-selector.html | 4 + .../index.js | 15 ++ .../editEdgeStackForm.html | 92 ++++--- .../editEdgeStackFormController.js | 78 +++++- .../group-form/groupFormController.js | 2 +- app/edge/services/edge-stack.js | 38 ++- .../editEdgeGroupViewController.js | 8 +- ...s => create-edge-stack-view.controller.js} | 145 ++++++----- .../create-edge-stack-view.html | 94 +++++++ .../create-edge-stack-view.js | 6 + .../createEdgeStackView.html | 220 ---------------- .../docker-compose-form.controller.js | 64 +++++ .../docker-compose-form.html | 74 ++++++ .../docker-compose-form/index.js | 11 + .../edge-stacks/createEdgeStackView/index.js | 15 +- .../kube-deploy-description/index.js | 3 + .../kube-deploy-description.html | 8 + .../kube-manifest-form/index.js | 11 + .../kube-manifest-form.controller.js | 29 +++ .../kube-manifest-form.html | 26 ++ .../editEdgeStackViewController.js | 6 +- app/edge/views/edge-stacks/index.js | 5 + .../associatedEndpointsSelectorController.js | 4 +- .../box-selector-item/box-selector-item.html | 20 +- .../box-selector/box-selector-item/index.js | 2 + .../components/box-selector/box-selector.css | 4 + .../components/box-selector/box-selector.html | 2 + .../git-form-auth-fieldset.html | 9 +- .../git-form-compose-path-field.html | 17 +- .../git-form-compose-path-field/index.js | 2 + .../git-form-ref-field.html | 4 +- .../git-form-url-field.html | 2 + .../forms/git-form/git-form.controller.js | 4 + .../components/forms/git-form/git-form.html | 15 +- .../components/forms/git-form/git-form.js | 1 + app/portainer/services/fileUpload.js | 8 +- app/portainer/services/notifications.js | 9 +- 56 files changed, 1466 insertions(+), 521 deletions(-) create mode 100644 api/http/handler/edgegroups/edgegroup_list_test.go create mode 100644 api/http/handler/edgestacks/edgestack_create_test.go create mode 100644 api/http/handler/edgestacks/endpoints.go create mode 100644 api/http/handler/edgestacks/endpoints_test.go create mode 100644 api/internal/endpointutils/endpoint_test.go create mode 100644 app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js create mode 100644 app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html create mode 100644 app/edge/components/edge-stack-deployment-type-selector/index.js rename app/edge/views/edge-stacks/createEdgeStackView/{createEdgeStackViewController.js => create-edge-stack-view.controller.js} (57%) create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js delete mode 100644 app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html create mode 100644 app/edge/views/edge-stacks/index.js diff --git a/api/http/handler/edgegroups/edgegroup_list.go b/api/http/handler/edgegroups/edgegroup_list.go index efaf7dd85..0363ce87e 100644 --- a/api/http/handler/edgegroups/edgegroup_list.go +++ b/api/http/handler/edgegroups/edgegroup_list.go @@ -1,6 +1,7 @@ package edgegroups import ( + "fmt" "net/http" httperror "github.com/portainer/libhttp/error" @@ -10,7 +11,8 @@ import ( type decoratedEdgeGroup struct { portainer.EdgeGroup - HasEdgeStack bool `json:"HasEdgeStack"` + HasEdgeStack bool `json:"HasEdgeStack"` + EndpointTypes []portainer.EndpointType } // @id EdgeGroupList @@ -46,17 +48,25 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h decoratedEdgeGroups := []decoratedEdgeGroup{} for _, orgEdgeGroup := range edgeGroups { edgeGroup := decoratedEdgeGroup{ - EdgeGroup: orgEdgeGroup, + EdgeGroup: orgEdgeGroup, + EndpointTypes: []portainer.EndpointType{}, } if edgeGroup.Dynamic { - endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) + endpointIDs, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments and environment groups for Edge group", err} } - edgeGroup.Endpoints = endpoints + edgeGroup.Endpoints = endpointIDs } + endpointTypes, err := getEndpointTypes(handler.DataStore.Endpoint(), edgeGroup.Endpoints) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint types for Edge group", err} + } + + edgeGroup.EndpointTypes = endpointTypes + edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID] decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup) @@ -64,3 +74,22 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h return response.JSON(w, decoratedEdgeGroups) } + +func getEndpointTypes(endpointService portainer.EndpointService, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) { + typeSet := map[portainer.EndpointType]bool{} + for _, endpointID := range endpointIds { + endpoint, err := endpointService.Endpoint(endpointID) + if err != nil { + return nil, fmt.Errorf("failed fetching endpoint: %w", err) + } + + typeSet[endpoint.Type] = true + } + + endpointTypes := make([]portainer.EndpointType, 0, len(typeSet)) + for endpointType := range typeSet { + endpointTypes = append(endpointTypes, endpointType) + } + + return endpointTypes, nil +} diff --git a/api/http/handler/edgegroups/edgegroup_list_test.go b/api/http/handler/edgegroups/edgegroup_list_test.go new file mode 100644 index 000000000..36816fea9 --- /dev/null +++ b/api/http/handler/edgegroups/edgegroup_list_test.go @@ -0,0 +1,53 @@ +package edgegroups + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/testhelpers" + "github.com/stretchr/testify/assert" +) + +func Test_getEndpointTypes(t *testing.T) { + endpoints := []portainer.Endpoint{ + {ID: 1, Type: portainer.DockerEnvironment}, + {ID: 2, Type: portainer.AgentOnDockerEnvironment}, + {ID: 3, Type: portainer.AzureEnvironment}, + {ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment}, + {ID: 5, Type: portainer.KubernetesLocalEnvironment}, + {ID: 6, Type: portainer.AgentOnKubernetesEnvironment}, + {ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment}, + } + + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints)) + + tests := []struct { + endpointIds []portainer.EndpointID + expected []portainer.EndpointType + }{ + {endpointIds: []portainer.EndpointID{1}, expected: []portainer.EndpointType{portainer.DockerEnvironment}}, + {endpointIds: []portainer.EndpointID{2}, expected: []portainer.EndpointType{portainer.AgentOnDockerEnvironment}}, + {endpointIds: []portainer.EndpointID{3}, expected: []portainer.EndpointType{portainer.AzureEnvironment}}, + {endpointIds: []portainer.EndpointID{4}, expected: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment}}, + {endpointIds: []portainer.EndpointID{5}, expected: []portainer.EndpointType{portainer.KubernetesLocalEnvironment}}, + {endpointIds: []portainer.EndpointID{6}, expected: []portainer.EndpointType{portainer.AgentOnKubernetesEnvironment}}, + {endpointIds: []portainer.EndpointID{7}, expected: []portainer.EndpointType{portainer.EdgeAgentOnKubernetesEnvironment}}, + {endpointIds: []portainer.EndpointID{7, 2}, expected: []portainer.EndpointType{portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnDockerEnvironment}}, + {endpointIds: []portainer.EndpointID{6, 4, 1}, expected: []portainer.EndpointType{portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnDockerEnvironment, portainer.DockerEnvironment}}, + {endpointIds: []portainer.EndpointID{1, 2, 3}, expected: []portainer.EndpointType{portainer.DockerEnvironment, portainer.AgentOnDockerEnvironment, portainer.AzureEnvironment}}, + } + + for _, test := range tests { + ans, err := getEndpointTypes(datastore.Endpoint(), test.endpointIds) + assert.NoError(t, err, "getEndpointTypes shouldn't fail") + + assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans) + } +} + +func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) { + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{})) + + _, err := getEndpointTypes(datastore.Endpoint(), []portainer.EndpointID{1}) + assert.Error(t, err, "getEndpointTypes should fail") +} diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 80e838827..4a3f5ce30 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -2,6 +2,7 @@ package edgestacks import ( "errors" + "fmt" "net/http" "strconv" "strings" @@ -42,37 +43,6 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err} } - endpoints, err := handler.DataStore.Endpoint().Endpoints() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} - } - - endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} - } - - edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} - } - - relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) - - for _, endpointID := range relatedEndpoints { - relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} - } - - relation.EdgeStacks[edgeStack.ID] = true - - err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err} - } - } - return response.JSON(w, edgeStack) } @@ -95,6 +65,11 @@ type swarmStackFromFileContentPayload struct { StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"` // List of identifiers of EdgeGroups EdgeGroups []portainer.EdgeGroupID `example:"1"` + // Deployment type to deploy this stack + // Valid values are: 0 - 'compose', 1 - 'kubernetes' + // for compose stacks will use kompose to convert to kubernetes manifest for kubernetes endpoints + // kubernetes deploytype is enabled only for kubernetes endpoints + DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1"` } func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error { @@ -124,21 +99,64 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ - ID: portainer.EdgeStackID(stackID), - Name: payload.Name, - EntryPoint: filesystem.ComposeFileDefaultName, - CreationDate: time.Now().Unix(), - EdgeGroups: payload.EdgeGroups, - Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), - Version: 1, + ID: portainer.EdgeStackID(stackID), + Name: payload.Name, + DeploymentType: payload.DeploymentType, + CreationDate: time.Now().Unix(), + EdgeGroups: payload.EdgeGroups, + Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), + Version: 1, + } + + relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore) + if err != nil { + return nil, fmt.Errorf("unable to find environment relations in database: %w", err) + } + + relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) + if err != nil { + return nil, fmt.Errorf("unable to persist environment relation in database: %w", err) } stackFolder := strconv.Itoa(int(stack.ID)) - projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) - if err != nil { - return nil, err + if stack.DeploymentType == portainer.EdgeStackDeploymentCompose { + stack.EntryPoint = filesystem.ComposeFileDefaultName + + projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) + if err != nil { + return nil, err + } + stack.ProjectPath = projectPath + + err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err) + } + + } else { + hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("unable to check for existence of docker endpoint: %w", err) + } + + if hasDockerEndpoint { + return nil, fmt.Errorf("edge stack with docker endpoint cannot be deployed with kubernetes config") + } + + stack.ManifestPath = filesystem.ManifestFileDefaultName + + projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent)) + if err != nil { + return nil, err + } + + stack.ProjectPath = projectPath + } + + err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Unable to update endpoint relations: %w", err) } - stack.ProjectPath = projectPath err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { @@ -162,9 +180,14 @@ type swarmStackFromGitRepositoryPayload struct { // Password used in basic authentication. Required when RepositoryAuthentication is true. RepositoryPassword string `example:"myGitPassword"` // Path to the Stack file inside the Git repository - ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` + FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` // List of identifiers of EdgeGroups EdgeGroups []portainer.EdgeGroupID `example:"1"` + // Deployment type to deploy this stack + // Valid values are: 0 - 'compose', 1 - 'kubernetes' + // for compose stacks will use kompose to convert to kubernetes manifest for kubernetes endpoints + // kubernetes deploytype is enabled only for kubernetes endpoints + DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1"` } func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { @@ -177,8 +200,8 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } - if govalidator.IsNull(payload.ComposeFilePathInRepository) { - payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName + if govalidator.IsNull(payload.FilePathInRepository) { + payload.FilePathInRepository = filesystem.ComposeFileDefaultName } if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 { return errors.New("Edge Groups are mandatory for an Edge stack") @@ -200,13 +223,13 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ - ID: portainer.EdgeStackID(stackID), - Name: payload.Name, - EntryPoint: payload.ComposeFilePathInRepository, - CreationDate: time.Now().Unix(), - EdgeGroups: payload.EdgeGroups, - Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), - Version: 1, + ID: portainer.EdgeStackID(stackID), + Name: payload.Name, + CreationDate: time.Now().Unix(), + EdgeGroups: payload.EdgeGroups, + Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), + DeploymentType: payload.DeploymentType, + Version: 1, } projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID))) @@ -219,11 +242,37 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por repositoryPassword = "" } + relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore) + if err != nil { + return nil, fmt.Errorf("failed fetching relations config: %w", err) + } + + relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) + if err != nil { + return nil, fmt.Errorf("unable to retrieve related endpoints: %w", err) + } + err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword) if err != nil { return nil, err } + if stack.DeploymentType == portainer.EdgeStackDeploymentCompose { + stack.EntryPoint = payload.FilePathInRepository + + err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err) + } + } else { + stack.ManifestPath = payload.FilePathInRepository + } + + err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Unable to update endpoint relations: %w", err) + } + err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { return nil, err @@ -236,6 +285,7 @@ type swarmStackFromFileUploadPayload struct { Name string StackFileContent []byte EdgeGroups []portainer.EdgeGroupID + DeploymentType portainer.EdgeStackDeploymentType } func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error { @@ -257,6 +307,13 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error return errors.New("Edge Groups are mandatory for an Edge stack") } payload.EdgeGroups = edgeGroups + + deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", true) + if err != nil { + return errors.New("Invalid deployment type") + } + payload.DeploymentType = portainer.EdgeStackDeploymentType(deploymentType) + return nil } @@ -274,21 +331,54 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ - ID: portainer.EdgeStackID(stackID), - Name: payload.Name, - EntryPoint: filesystem.ComposeFileDefaultName, - CreationDate: time.Now().Unix(), - EdgeGroups: payload.EdgeGroups, - Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), - Version: 1, + ID: portainer.EdgeStackID(stackID), + Name: payload.Name, + DeploymentType: payload.DeploymentType, + CreationDate: time.Now().Unix(), + EdgeGroups: payload.EdgeGroups, + Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), + Version: 1, + } + + relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore) + if err != nil { + return nil, fmt.Errorf("failed fetching relations config: %w", err) + } + + relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) + if err != nil { + return nil, fmt.Errorf("unable to retrieve related endpoints: %w", err) } stackFolder := strconv.Itoa(int(stack.ID)) - projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) - if err != nil { - return nil, err + if stack.DeploymentType == portainer.EdgeStackDeploymentCompose { + stack.EntryPoint = filesystem.ComposeFileDefaultName + + projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) + if err != nil { + return nil, err + } + stack.ProjectPath = projectPath + + err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err) + } + + } else { + stack.ManifestPath = filesystem.ManifestFileDefaultName + + projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent)) + if err != nil { + return nil, err + } + stack.ProjectPath = projectPath + } + + err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds) + if err != nil { + return nil, fmt.Errorf("Unable to update endpoint relations: %w", err) } - stack.ProjectPath = projectPath err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { @@ -311,3 +401,22 @@ func (handler *Handler) validateUniqueName(name string) error { } return nil } + +// updateEndpointRelations adds a relation between the Edge Stack to the related endpoints +func updateEndpointRelations(endpointRelationService portainer.EndpointRelationService, edgeStackID portainer.EdgeStackID, relatedEndpointIds []portainer.EndpointID) error { + for _, endpointID := range relatedEndpointIds { + relation, err := endpointRelationService.EndpointRelation(endpointID) + if err != nil { + return fmt.Errorf("unable to find endpoint relation in database: %w", err) + } + + relation.EdgeStacks[edgeStackID] = true + + err = endpointRelationService.UpdateEndpointRelation(endpointID, relation) + if err != nil { + return fmt.Errorf("unable to persist endpoint relation in database: %w", err) + } + } + + return nil +} diff --git a/api/http/handler/edgestacks/edgestack_create_test.go b/api/http/handler/edgestacks/edgestack_create_test.go new file mode 100644 index 000000000..8e1144e4a --- /dev/null +++ b/api/http/handler/edgestacks/edgestack_create_test.go @@ -0,0 +1,38 @@ +package edgestacks + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/testhelpers" + "github.com/stretchr/testify/assert" +) + +func Test_updateEndpointRelation_successfulRuns(t *testing.T) { + edgeStackID := portainer.EdgeStackID(5) + endpointRelations := []portainer.EndpointRelation{ + {EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]bool{}}, + {EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]bool{}}, + {EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]bool{}}, + {EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]bool{}}, + {EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]bool{}}, + } + + relatedIds := []portainer.EndpointID{2, 3} + + dataStore := testhelpers.NewDatastore(testhelpers.WithEndpointRelations(endpointRelations)) + + err := updateEndpointRelations(dataStore.EndpointRelation(), edgeStackID, relatedIds) + + assert.NoError(t, err, "updateEndpointRelations should not fail") + + relatedSet := map[portainer.EndpointID]bool{} + for _, relationID := range relatedIds { + relatedSet[relationID] = true + } + + for _, relation := range endpointRelations { + shouldBeRelated := relatedSet[relation.EndpointID] + assert.Equal(t, shouldBeRelated, relation.EdgeStacks[edgeStackID]) + } +} diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index 7ff0ee1cd..f9269705b 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -42,24 +42,17 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err} } - endpoints, err := handler.DataStore.Endpoint().Endpoints() + relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments relations config from database", err} } - endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() + relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} } - edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} - } - - relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) - - for _, endpointID := range relatedEndpoints { + for _, endpointID := range relatedEndpointIds { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err} diff --git a/api/http/handler/edgestacks/edgestack_file.go b/api/http/handler/edgestacks/edgestack_file.go index 02bfbbe84..e4adee8d8 100644 --- a/api/http/handler/edgestacks/edgestack_file.go +++ b/api/http/handler/edgestacks/edgestack_file.go @@ -41,7 +41,12 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} } - stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint)) + fileName := stack.EntryPoint + if stack.DeploymentType == portainer.EdgeStackDeploymentKubernetes { + fileName = stack.ManifestPath + } + + stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err} } diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index e81fe5579..f2d2bd329 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -5,24 +5,24 @@ import ( "net/http" "strconv" - "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/internal/edge" ) type updateEdgeStackPayload struct { StackFileContent string Version *int - Prune *bool EdgeGroups []portainer.EdgeGroupID + DeploymentType portainer.EdgeStackDeploymentType } func (payload *updateEdgeStackPayload) Validate(r *http.Request) error { - if govalidator.IsNull(payload.StackFileContent) { + if payload.StackFileContent == "" { return errors.New("Invalid stack file content") } if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 { @@ -64,33 +64,23 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } + relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments relations config from database", err} + } + + relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} + } + if payload.EdgeGroups != nil { - endpoints, err := handler.DataStore.Endpoint().Endpoints() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err} - } - - endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err} - } - - edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} - } - - oldRelated, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups) + newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} } - newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err} - } - - oldRelatedSet := EndpointSet(oldRelated) + oldRelatedSet := EndpointSet(relatedEndpointIds) newRelatedSet := EndpointSet(newRelated) endpointsToRemove := map[portainer.EndpointID]bool{} @@ -136,17 +126,55 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) } stack.EdgeGroups = payload.EdgeGroups - + relatedEndpointIds = newRelated } - if payload.Prune != nil { - stack.Prune = *payload.Prune + if stack.DeploymentType != payload.DeploymentType { + // deployment type was changed - need to delete the old file + err = handler.FileService.RemoveDirectory(stack.ProjectPath) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clear old files", err} + } + + stack.EntryPoint = "" + stack.ManifestPath = "" + stack.DeploymentType = payload.DeploymentType } stackFolder := strconv.Itoa(int(stack.ID)) - _, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err} + if payload.DeploymentType == portainer.EdgeStackDeploymentCompose { + if stack.EntryPoint == "" { + stack.EntryPoint = filesystem.ComposeFileDefaultName + } + + _, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err} + } + + err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to convert and persist updated Kubernetes manifest file on disk", err} + } + + } else { + if stack.ManifestPath == "" { + stack.ManifestPath = filesystem.ManifestFileDefaultName + } + + hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for existence of docker endpoint", err} + } + + if hasDockerEndpoint { + return &httperror.HandlerError{http.StatusBadRequest, "Edge stack with docker endpoint cannot be deployed with kubernetes config", err} + } + + _, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err} + } } if payload.Version != nil && *payload.Version != stack.Version { diff --git a/api/http/handler/edgestacks/endpoints.go b/api/http/handler/edgestacks/endpoints.go new file mode 100644 index 000000000..de494f9f0 --- /dev/null +++ b/api/http/handler/edgestacks/endpoints.go @@ -0,0 +1,60 @@ +package edgestacks + +import ( + "fmt" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/endpointutils" +) + +func hasKubeEndpoint(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID) (bool, error) { + return hasEndpointPredicate(endpointService, endpointIDs, endpointutils.IsKubernetesEndpoint) +} + +func hasDockerEndpoint(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID) (bool, error) { + return hasEndpointPredicate(endpointService, endpointIDs, endpointutils.IsDockerEndpoint) +} + +func hasEndpointPredicate(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID, predicate func(*portainer.Endpoint) bool) (bool, error) { + for _, endpointID := range endpointIDs { + endpoint, err := endpointService.Endpoint(endpointID) + if err != nil { + return false, fmt.Errorf("failed to retrieve endpoint from database: %w", err) + } + + if predicate(endpoint) { + return true, nil + } + } + + return false, nil +} + +type endpointRelationsConfig struct { + endpoints []portainer.Endpoint + endpointGroups []portainer.EndpointGroup + edgeGroups []portainer.EdgeGroup +} + +func fetchEndpointRelationsConfig(dataStore portainer.DataStore) (*endpointRelationsConfig, error) { + endpoints, err := dataStore.Endpoint().Endpoints() + if err != nil { + return nil, fmt.Errorf("unable to retrieve environments from database: %w", err) + } + + endpointGroups, err := dataStore.EndpointGroup().EndpointGroups() + if err != nil { + return nil, fmt.Errorf("unable to retrieve environment groups from database: %w", err) + } + + edgeGroups, err := dataStore.EdgeGroup().EdgeGroups() + if err != nil { + return nil, fmt.Errorf("unable to retrieve edge groups from database: %w", err) + } + + return &endpointRelationsConfig{ + endpoints: endpoints, + endpointGroups: endpointGroups, + edgeGroups: edgeGroups, + }, nil +} diff --git a/api/http/handler/edgestacks/endpoints_test.go b/api/http/handler/edgestacks/endpoints_test.go new file mode 100644 index 000000000..6cc94b17c --- /dev/null +++ b/api/http/handler/edgestacks/endpoints_test.go @@ -0,0 +1,99 @@ +package edgestacks + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/testhelpers" + "github.com/stretchr/testify/assert" +) + +func Test_hasKubeEndpoint(t *testing.T) { + endpoints := []portainer.Endpoint{ + {ID: 1, Type: portainer.DockerEnvironment}, + {ID: 2, Type: portainer.AgentOnDockerEnvironment}, + {ID: 3, Type: portainer.AzureEnvironment}, + {ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment}, + {ID: 5, Type: portainer.KubernetesLocalEnvironment}, + {ID: 6, Type: portainer.AgentOnKubernetesEnvironment}, + {ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment}, + } + + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints)) + + tests := []struct { + endpointIds []portainer.EndpointID + expected bool + }{ + {endpointIds: []portainer.EndpointID{1}, expected: false}, + {endpointIds: []portainer.EndpointID{2}, expected: false}, + {endpointIds: []portainer.EndpointID{3}, expected: false}, + {endpointIds: []portainer.EndpointID{4}, expected: false}, + {endpointIds: []portainer.EndpointID{5}, expected: true}, + {endpointIds: []portainer.EndpointID{6}, expected: true}, + {endpointIds: []portainer.EndpointID{7}, expected: true}, + {endpointIds: []portainer.EndpointID{7, 2}, expected: true}, + {endpointIds: []portainer.EndpointID{6, 4, 1}, expected: true}, + {endpointIds: []portainer.EndpointID{1, 2, 3}, expected: false}, + } + + for _, test := range tests { + + ans, err := hasKubeEndpoint(datastore.Endpoint(), test.endpointIds) + assert.NoError(t, err, "hasKubeEndpoint shouldn't fail") + + assert.Equal(t, test.expected, ans, "hasKubeEndpoint expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans) + } +} + +func Test_hasKubeEndpoint_failWhenEndpointDontExist(t *testing.T) { + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{})) + + _, err := hasKubeEndpoint(datastore.Endpoint(), []portainer.EndpointID{1}) + assert.Error(t, err, "hasKubeEndpoint should fail") +} + +func Test_hasDockerEndpoint(t *testing.T) { + endpoints := []portainer.Endpoint{ + {ID: 1, Type: portainer.DockerEnvironment}, + {ID: 2, Type: portainer.AgentOnDockerEnvironment}, + {ID: 3, Type: portainer.AzureEnvironment}, + {ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment}, + {ID: 5, Type: portainer.KubernetesLocalEnvironment}, + {ID: 6, Type: portainer.AgentOnKubernetesEnvironment}, + {ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment}, + } + + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints)) + + tests := []struct { + endpointIds []portainer.EndpointID + expected bool + }{ + {endpointIds: []portainer.EndpointID{1}, expected: true}, + {endpointIds: []portainer.EndpointID{2}, expected: true}, + {endpointIds: []portainer.EndpointID{3}, expected: false}, + {endpointIds: []portainer.EndpointID{4}, expected: true}, + {endpointIds: []portainer.EndpointID{5}, expected: false}, + {endpointIds: []portainer.EndpointID{6}, expected: false}, + {endpointIds: []portainer.EndpointID{7}, expected: false}, + {endpointIds: []portainer.EndpointID{7, 2}, expected: true}, + {endpointIds: []portainer.EndpointID{6, 4, 1}, expected: true}, + {endpointIds: []portainer.EndpointID{1, 2, 3}, expected: true}, + } + + for _, test := range tests { + + ans, err := hasDockerEndpoint(datastore.Endpoint(), test.endpointIds) + assert.NoError(t, err, "hasDockerEndpoint shouldn't fail") + + assert.Equal(t, test.expected, ans, "hasDockerEndpoint expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans) + } +} + +func Test_hasDockerEndpoint_failWhenEndpointDontExist(t *testing.T) { + datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{})) + + _, err := hasDockerEndpoint(datastore.Endpoint(), []portainer.EndpointID{1}) + assert.Error(t, err, "hasDockerEndpoint should fail") +} diff --git a/api/http/handler/edgestacks/handler.go b/api/http/handler/edgestacks/handler.go index 2e0580d6d..5e14719db 100644 --- a/api/http/handler/edgestacks/handler.go +++ b/api/http/handler/edgestacks/handler.go @@ -1,21 +1,26 @@ package edgestacks import ( + "fmt" "net/http" + "path" + "strconv" "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - DataStore portainer.DataStore - FileService portainer.FileService - GitService portainer.GitService + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore + FileService portainer.FileService + GitService portainer.GitService + KubernetesDeployer portainer.KubernetesDeployer } // NewHandler creates a handler to manage endpoint group operations. @@ -40,3 +45,34 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut) return h } + +func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer.EdgeStack, relatedEndpointIds []portainer.EndpointID) error { + hasKubeEndpoint, err := hasKubeEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds) + if err != nil { + return fmt.Errorf("unable to check if edge stack has kube endpoints: %w", err) + } + + if !hasKubeEndpoint { + return nil + } + + composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint)) + if err != nil { + return fmt.Errorf("unable to retrieve Compose file from disk: %w", err) + } + + kompose, err := handler.KubernetesDeployer.ConvertCompose(composeConfig) + if err != nil { + return fmt.Errorf("failed converting compose file to kubernetes manifest: %w", err) + } + + komposeFileName := filesystem.ManifestFileDefaultName + _, err = handler.FileService.StoreEdgeStackFileFromBytes(strconv.Itoa(int(edgeStack.ID)), komposeFileName, kompose) + if err != nil { + return fmt.Errorf("failed to store kube manifest file: %w", err) + } + + edgeStack.ManifestPath = komposeFileName + + return nil +} diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 823f21c98..8f749fb1b 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -1,6 +1,7 @@ package endpointedge import ( + "errors" "net/http" "path" @@ -8,11 +9,11 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/bolt/errors" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/internal/endpointutils" ) type configResponse struct { - Prune bool StackFileContent string Name string } @@ -36,7 +37,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == errors.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} @@ -53,19 +54,33 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. } edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) - if err == errors.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} } - stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint)) + fileName := edgeStack.EntryPoint + if endpointutils.IsDockerEndpoint(endpoint) { + if fileName == "" { + return &httperror.HandlerError{http.StatusBadRequest, "Docker is not supported by this stack", errors.New("Docker is not supported by this stack")} + } + } + + if endpointutils.IsKubernetesEndpoint(endpoint) { + fileName = edgeStack.ManifestPath + + if fileName == "" { + return &httperror.HandlerError{http.StatusBadRequest, "Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack")} + } + } + + stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err} } return response.JSON(w, configResponse{ - Prune: edgeStack.Prune, StackFileContent: string(stackFileContent), Name: edgeStack.Name, }) diff --git a/api/http/server.go b/api/http/server.go index 88ca1284f..d826760f0 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -130,6 +130,7 @@ func (server *Server) Start() error { edgeStacksHandler.DataStore = server.DataStore edgeStacksHandler.FileService = server.FileService edgeStacksHandler.GitService = server.GitService + edgeStacksHandler.KubernetesDeployer = server.KubernetesDeployer var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer) edgeTemplatesHandler.DataStore = server.DataStore diff --git a/api/internal/endpointutils/endpoint_test.go b/api/internal/endpointutils/endpoint_test.go new file mode 100644 index 000000000..35793b6e5 --- /dev/null +++ b/api/internal/endpointutils/endpoint_test.go @@ -0,0 +1,47 @@ +package endpointutils + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" +) + +type isEndpointTypeTest struct { + endpointType portainer.EndpointType + expected bool +} + +func Test_IsDockerEndpoint(t *testing.T) { + tests := []isEndpointTypeTest{ + {endpointType: portainer.DockerEnvironment, expected: true}, + {endpointType: portainer.AgentOnDockerEnvironment, expected: true}, + {endpointType: portainer.AzureEnvironment, expected: false}, + {endpointType: portainer.EdgeAgentOnDockerEnvironment, expected: true}, + {endpointType: portainer.KubernetesLocalEnvironment, expected: false}, + {endpointType: portainer.AgentOnKubernetesEnvironment, expected: false}, + {endpointType: portainer.EdgeAgentOnKubernetesEnvironment, expected: false}, + } + + for _, test := range tests { + ans := IsDockerEndpoint(&portainer.Endpoint{Type: test.endpointType}) + assert.Equal(t, test.expected, ans) + } +} + +func Test_IsKubernetesEndpoint(t *testing.T) { + tests := []isEndpointTypeTest{ + {endpointType: portainer.DockerEnvironment, expected: false}, + {endpointType: portainer.AgentOnDockerEnvironment, expected: false}, + {endpointType: portainer.AzureEnvironment, expected: false}, + {endpointType: portainer.EdgeAgentOnDockerEnvironment, expected: false}, + {endpointType: portainer.KubernetesLocalEnvironment, expected: true}, + {endpointType: portainer.AgentOnKubernetesEnvironment, expected: true}, + {endpointType: portainer.EdgeAgentOnKubernetesEnvironment, expected: true}, + } + + for _, test := range tests { + ans := IsKubernetesEndpoint(&portainer.Endpoint{Type: test.endpointType}) + assert.Equal(t, test.expected, ans) + } +} diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go index 08e865c77..89e3b0329 100644 --- a/api/internal/testhelpers/datastore.go +++ b/api/internal/testhelpers/datastore.go @@ -4,6 +4,7 @@ import ( "io" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) type datastore struct { @@ -127,3 +128,106 @@ func WithEdgeJobs(js []portainer.EdgeJob) datastoreOption { d.edgeJob = &stubEdgeJobService{jobs: js} } } + +type stubEndpointRelationService struct { + relations []portainer.EndpointRelation +} + +func (s *stubEndpointRelationService) EndpointRelations() ([]portainer.EndpointRelation, error) { + return s.relations, nil +} +func (s *stubEndpointRelationService) EndpointRelation(ID portainer.EndpointID) (*portainer.EndpointRelation, error) { + for _, relation := range s.relations { + if relation.EndpointID == ID { + return &relation, nil + } + } + + return nil, errors.ErrObjectNotFound +} +func (s *stubEndpointRelationService) CreateEndpointRelation(EndpointRelation *portainer.EndpointRelation) error { + return nil +} +func (s *stubEndpointRelationService) UpdateEndpointRelation(ID portainer.EndpointID, relation *portainer.EndpointRelation) error { + for i, r := range s.relations { + if r.EndpointID == ID { + s.relations[i] = *relation + } + } + + return nil +} +func (s *stubEndpointRelationService) DeleteEndpointRelation(ID portainer.EndpointID) error { + return nil +} +func (s *stubEndpointRelationService) GetNextIdentifier() int { return 0 } + +// WithEndpointRelations option will instruct datastore to return provided jobs +func WithEndpointRelations(relations []portainer.EndpointRelation) datastoreOption { + return func(d *datastore) { + d.endpointRelation = &stubEndpointRelationService{relations: relations} + } +} + +type stubEndpointService struct { + endpoints []portainer.Endpoint +} + +func (s *stubEndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) { + for _, endpoint := range s.endpoints { + if endpoint.ID == ID { + return &endpoint, nil + } + } + + return nil, errors.ErrObjectNotFound +} + +func (s *stubEndpointService) Endpoints() ([]portainer.Endpoint, error) { + return s.endpoints, nil +} + +func (s *stubEndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error { + s.endpoints = append(s.endpoints, *endpoint) + + return nil +} + +func (s *stubEndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error { + for i, e := range s.endpoints { + if e.ID == ID { + s.endpoints[i] = *endpoint + } + } + + return nil +} + +func (s *stubEndpointService) DeleteEndpoint(ID portainer.EndpointID) error { + endpoints := []portainer.Endpoint{} + + for _, endpoint := range s.endpoints { + if endpoint.ID != ID { + endpoints = append(endpoints, endpoint) + } + } + + s.endpoints = endpoints + + return nil +} + +func (s *stubEndpointService) Synchronize(toCreate []*portainer.Endpoint, toUpdate []*portainer.Endpoint, toDelete []*portainer.Endpoint) error { + panic("not implemented") +} + +func (s *stubEndpointService) GetNextIdentifier() int { + return len(s.endpoints) +} + +// WithEndpoints option will instruct datastore to return provided endpoints +func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption { + return func(d *datastore) { + d.endpoint = &stubEndpointService{endpoints: endpoints} + } +} diff --git a/api/portainer.go b/api/portainer.go index 96af4f17e..703b3fd36 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -194,17 +194,23 @@ type ( //EdgeStack represents an edge stack EdgeStack struct { // EdgeStack Identifier - ID EdgeStackID `json:"Id" example:"1"` - Name string `json:"Name"` - Status map[EndpointID]EdgeStackStatus `json:"Status"` - CreationDate int64 `json:"CreationDate"` - EdgeGroups []EdgeGroupID `json:"EdgeGroups"` - ProjectPath string `json:"ProjectPath"` - EntryPoint string `json:"EntryPoint"` - Version int `json:"Version"` - Prune bool `json:"Prune"` + ID EdgeStackID `json:"Id" example:"1"` + Name string `json:"Name"` + Status map[EndpointID]EdgeStackStatus `json:"Status"` + CreationDate int64 `json:"CreationDate"` + EdgeGroups []EdgeGroupID `json:"EdgeGroups"` + ProjectPath string `json:"ProjectPath"` + EntryPoint string `json:"EntryPoint"` + Version int `json:"Version"` + ManifestPath string + DeploymentType EdgeStackDeploymentType + + // Deprecated + Prune bool `json:"Prune"` } + EdgeStackDeploymentType int + //EdgeStackID represents an edge stack id EdgeStackID int @@ -1502,6 +1508,13 @@ const ( CustomTemplatePlatformWindows ) +const ( + // EdgeStackDeploymentCompose represent an edge stack deployed using a compose file + EdgeStackDeploymentCompose EdgeStackDeploymentType = iota + // EdgeStackDeploymentKubernetes represent an edge stack deployed using a kubernetes manifest file + EdgeStackDeploymentKubernetes +) + const ( _ EdgeStackStatusType = iota //StatusOk represents a successfully deployed edge stack diff --git a/app/edge/__module.js b/app/edge/__module.js index a1507f356..72af140a5 100644 --- a/app/edge/__module.js +++ b/app/edge/__module.js @@ -1,6 +1,8 @@ import angular from 'angular'; -angular.module('portainer.edge', []).config(function config($stateRegistryProvider) { +import edgeStackModule from './views/edge-stacks'; + +angular.module('portainer.edge', [edgeStackModule]).config(function config($stateRegistryProvider) { const edge = { name: 'edge', url: '/edge', diff --git a/app/edge/components/edge-groups-selector/edgeGroupsSelector.html b/app/edge/components/edge-groups-selector/edgeGroupsSelector.html index 3ecb421a0..1210f3cb0 100644 --- a/app/edge/components/edge-groups-selector/edgeGroupsSelector.html +++ b/app/edge/components/edge-groups-selector/edgeGroupsSelector.html @@ -1,4 +1,12 @@ - + + {{ $item.Name }} diff --git a/app/edge/components/edge-groups-selector/index.js b/app/edge/components/edge-groups-selector/index.js index 0a7f7cbb3..902139191 100644 --- a/app/edge/components/edge-groups-selector/index.js +++ b/app/edge/components/edge-groups-selector/index.js @@ -3,7 +3,8 @@ import angular from 'angular'; angular.module('portainer.edge').component('edgeGroupsSelector', { templateUrl: './edgeGroupsSelector.html', bindings: { - model: '=', + model: '<', items: '<', + onChange: '<', }, }); diff --git a/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js new file mode 100644 index 000000000..3cedd0464 --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.controller.js @@ -0,0 +1,21 @@ +export default class EdgeStackDeploymentTypeSelectorController { + /* @ngInject */ + constructor() { + this.deploymentOptions = [ + { id: 'deployment_compose', icon: 'fab fa-docker', label: 'Compose', description: 'docker-compose format', value: 0 }, + { + id: 'deployment_kube', + icon: 'fa fa-cubes', + label: 'Kubernetes', + description: 'Kubernetes manifest format', + value: 1, + disabled: () => { + return this.hasDockerEndpoint(); + }, + tooltip: () => { + return this.hasDockerEndpoint() ? 'Cannot use this option with Edge Docker endpoints' : ''; + }, + }, + ]; + } +} diff --git a/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html new file mode 100644 index 000000000..4eae73cc5 --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/edge-stack-deployment-type-selector.html @@ -0,0 +1,4 @@ +
+ Deployment type +
+ diff --git a/app/edge/components/edge-stack-deployment-type-selector/index.js b/app/edge/components/edge-stack-deployment-type-selector/index.js new file mode 100644 index 000000000..c175249fd --- /dev/null +++ b/app/edge/components/edge-stack-deployment-type-selector/index.js @@ -0,0 +1,15 @@ +import angular from 'angular'; +import controller from './edge-stack-deployment-type-selector.controller.js'; + +export const edgeStackDeploymentTypeSelector = { + templateUrl: './edge-stack-deployment-type-selector.html', + controller, + + bindings: { + value: '<', + onChange: '<', + hasDockerEndpoint: '<', + }, +}; + +angular.module('portainer.edge').component('edgeStackDeploymentTypeSelector', edgeStackDeploymentTypeSelector); diff --git a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html b/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html index e21b87c0e..95eb4f316 100644 --- a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html +++ b/app/edge/components/edit-edge-stack-form/editEdgeStackForm.html @@ -4,52 +4,70 @@
- +
- -
- Web editor -
-
- - You can get more information about Compose file format in the - - official documentation - - . - -
-
+
- +
+ + One or more of the selected Edge group contains Edge Docker endpoints that cannot be used with a Kubernetes Edge stack. +
- -
- Options -
-
+ + +
- - +
+ + Portainer uses Kompose to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not all the + Compose format options are supported by Kompose at the moment. +
+ + +
+ You can get more information about Compose file format in the + + official documentation + + . +
+
+
+ + + +

+ You can get more information about Kubernetes file format in the + official documentation. +

+
+
+
Actions @@ -59,9 +77,7 @@ + + {{ $ctrl.state.formValidationError }} + +
+
+ + + + +
+
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js new file mode 100644 index 000000000..7da77fb05 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.js @@ -0,0 +1,6 @@ +import controller from './create-edge-stack-view.controller'; + +export const createEdgeStackView = { + templateUrl: './create-edge-stack-view.html', + controller, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html deleted file mode 100644 index 916bcc1a7..000000000 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html +++ /dev/null @@ -1,220 +0,0 @@ - - - Edge Stacks > Create Edge stack - - -
-
- - -
- -
- -
- -
-
- - -
- Edge Groups -
-
-
- -
-
- No Edge groups are available. Head over to the Edge groups view to create one. -
-
- -
- Build method -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- Web editor -
-
- - You can get more information about Compose file format in the - - official documentation - - . - -
-
-
- -
-
-
- - -
-
- Upload -
-
- - You can upload a Compose file from your computer. - -
-
-
- - - {{ $ctrl.formValues.StackFile.name }} - - -
-
-
- - - - - -
-
- -
- -
-
- -
-
- Information -
-
-
-
-
-
-
- - -
-
- Web editor -
-
-
- -
-
-
-
- - - -
- Actions -
-
-
- - - {{ $ctrl.state.formValidationError }} - -
-
- - -
-
-
-
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js new file mode 100644 index 000000000..c78caa495 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js @@ -0,0 +1,64 @@ +class DockerComposeFormController { + /* @ngInject */ + constructor($async, EdgeTemplateService, Notifications) { + Object.assign(this, { $async, EdgeTemplateService, Notifications }); + + this.methodOptions = [ + { id: 'method_editor', icon: 'fa fa-edit', label: 'Web editor', description: 'Use our Web editor', value: 'editor' }, + { id: 'method_upload', icon: 'fa fa-upload', label: 'Upload', description: 'Upload from your computer', value: 'upload' }, + { id: 'method_repository', icon: 'fab fa-github', label: 'Repository', description: 'Use a git repository', value: 'repository' }, + { id: 'method_template', icon: 'fa fa-rocket', label: 'Template', description: 'Use an Edge stack template', value: 'template' }, + ]; + + this.selectedTemplate = null; + + this.onChangeFileContent = this.onChangeFileContent.bind(this); + this.onChangeFile = this.onChangeFile.bind(this); + this.onChangeTemplate = this.onChangeTemplate.bind(this); + this.onChangeMethod = this.onChangeMethod.bind(this); + this.onChangeFormValues = this.onChangeFormValues.bind(this); + } + + onChangeFormValues(values) { + this.formValues = values; + } + + onChangeMethod() { + this.formValues.StackFileContent = ''; + this.selectedTemplate = null; + } + + onChangeTemplate(template) { + return this.$async(async () => { + this.formValues.StackFileContent = ''; + try { + const fileContent = await this.EdgeTemplateService.edgeTemplate(template); + this.formValues.StackFileContent = fileContent; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve Template'); + } + }); + } + + onChangeFileContent(value) { + this.formValues.StackFileContent = value; + this.state.isEditorDirty = true; + } + + onChangeFile(value) { + this.formValues.StackFile = value; + } + + async $onInit() { + return this.$async(async () => { + try { + const templates = await this.EdgeTemplateService.edgeTemplates(); + this.templates = templates.map((template) => ({ ...template, label: `${template.title} - ${template.description}` })); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve Templates'); + } + }); + } +} + +export default DockerComposeFormController; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html new file mode 100644 index 000000000..48388c9dc --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html @@ -0,0 +1,74 @@ +
+ Build method +
+ + + + + You can get more information about Compose file format in the + + official documentation + + . + + + + + + You can upload a Compose file from your computer. + + + + + + +
+
+ +
+ +
+
+ +
+
+ Information +
+
+
+
+
+
+
+ + + + + + +
diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js new file mode 100644 index 000000000..59fd0aecc --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/index.js @@ -0,0 +1,11 @@ +import controller from './docker-compose-form.controller.js'; + +export const edgeStacksDockerComposeForm = { + templateUrl: './docker-compose-form.html', + controller, + + bindings: { + formValues: '=', + state: '=', + }, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/index.js b/app/edge/views/edge-stacks/createEdgeStackView/index.js index 29206fe54..af8eee573 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/index.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/index.js @@ -1,8 +1,13 @@ import angular from 'angular'; -import { CreateEdgeStackViewController } from './createEdgeStackViewController'; +import { createEdgeStackView } from './create-edge-stack-view'; +import { edgeStacksDockerComposeForm } from './docker-compose-form'; +import { kubeManifestForm } from './kube-manifest-form'; +import { kubeDeployDescription } from './kube-deploy-description'; -angular.module('portainer.edge').component('createEdgeStackView', { - templateUrl: './createEdgeStackView.html', - controller: CreateEdgeStackViewController, -}); +export default angular + .module('portainer.edge.stacks.create', []) + .component('createEdgeStackView', createEdgeStackView) + .component('edgeStacksDockerComposeForm', edgeStacksDockerComposeForm) + .component('edgeStacksKubeManifestForm', kubeManifestForm) + .component('kubeDeployDescription', kubeDeployDescription).name; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js new file mode 100644 index 000000000..53d8f3dd5 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/index.js @@ -0,0 +1,3 @@ +export const kubeDeployDescription = { + templateUrl: './kube-deploy-description.html', +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html new file mode 100644 index 000000000..c12d7ab22 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-deploy-description/kube-deploy-description.html @@ -0,0 +1,8 @@ +

+ + This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). +

+

+ You can get more information about Kubernetes file format in the + official documentation. +

diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js new file mode 100644 index 000000000..41949f7e3 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/index.js @@ -0,0 +1,11 @@ +import controller from './kube-manifest-form.controller.js'; + +export const kubeManifestForm = { + templateUrl: './kube-manifest-form.html', + controller, + + bindings: { + formValues: '=', + state: '=', + }, +}; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js new file mode 100644 index 000000000..5c3719cac --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js @@ -0,0 +1,29 @@ +class KubeManifestFormController { + /* @ngInject */ + constructor() { + this.methodOptions = [ + { id: 'method_editor', icon: 'fa fa-edit', label: 'Web editor', description: 'Use our Web editor', value: 'editor' }, + { id: 'method_upload', icon: 'fa fa-upload', label: 'Upload', description: 'Upload from your computer', value: 'upload' }, + { id: 'method_repository', icon: 'fab fa-github', label: 'Repository', description: 'Use a git repository', value: 'repository' }, + ]; + + this.onChangeFileContent = this.onChangeFileContent.bind(this); + this.onChangeFormValues = this.onChangeFormValues.bind(this); + this.onChangeFile = this.onChangeFile.bind(this); + } + + onChangeFormValues(values) { + this.formValues = values; + } + + onChangeFileContent(value) { + this.state.isEditorDirty = true; + this.formValues.StackFileContent = value; + } + + onChangeFile(value) { + this.formValues.StackFile = value; + } +} + +export default KubeManifestFormController; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html new file mode 100644 index 000000000..1b09a0712 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.html @@ -0,0 +1,26 @@ +
+ Build method +
+ + + + + + + + + + + + + + + diff --git a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js index 6fe162470..742b31728 100644 --- a/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js @@ -39,7 +39,7 @@ export class EditEdgeStackViewController { this.formValues = { StackFileContent: file, EdgeGroups: this.stack.EdgeGroups, - Prune: this.stack.Prune, + DeploymentType: this.stack.DeploymentType, }; this.oldFileContent = this.formValues.StackFileContent; } catch (err) { @@ -58,7 +58,7 @@ export class EditEdgeStackViewController { } async uiCanExit() { - if (this.formValues.StackFileContent !== this.oldFileContent && this.state.isEditorDirty) { + if (this.formValues.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== this.oldFileContent.replace(/(\r\n|\n|\r)/gm, '') && this.state.isEditorDirty) { return this.ModalService.confirmWebEditorDiscard(); } } @@ -99,7 +99,7 @@ export class EditEdgeStackViewController { async getPaginatedEndpointsAsync(lastId, limit, search) { try { - const query = { search, types: [4], endpointIds: this.stackEndpointIds }; + const query = { search, types: [4, 7], endpointIds: this.stackEndpointIds }; const { value, totalCount } = await this.EndpointService.endpoints(lastId, limit, query); const endpoints = _.map(value, (endpoint) => { const status = this.stack.Status[endpoint.Id]; diff --git a/app/edge/views/edge-stacks/index.js b/app/edge/views/edge-stacks/index.js new file mode 100644 index 000000000..ed21f54f6 --- /dev/null +++ b/app/edge/views/edge-stacks/index.js @@ -0,0 +1,5 @@ +import angular from 'angular'; + +import createModule from './createEdgeStackView'; + +export default angular.module('portainer.edge.stacks', [createModule]).name; diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js index 7d23a6d5e..c051d46c0 100644 --- a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js @@ -56,7 +56,7 @@ class AssoicatedEndpointsSelectorController { async getEndpointsAsync() { const { start, search, limit } = this.getPaginationData('available'); - const query = { search, types: [4] }; + const query = { search, types: [4, 7] }; const response = await this.EndpointService.endpoints(start, limit, query); @@ -73,7 +73,7 @@ class AssoicatedEndpointsSelectorController { let response = { value: [], totalCount: 0 }; if (this.endpointIds.length > 0) { const { start, search, limit } = this.getPaginationData('associated'); - const query = { search, types: [4], endpointIds: this.endpointIds }; + const query = { search, types: [4, 7], endpointIds: this.endpointIds }; response = await this.EndpointService.endpoints(start, limit, query); } diff --git a/app/portainer/components/box-selector/box-selector-item/box-selector-item.html b/app/portainer/components/box-selector/box-selector-item/box-selector-item.html index 398f65563..b6ff6c670 100644 --- a/app/portainer/components/box-selector/box-selector-item/box-selector-item.html +++ b/app/portainer/components/box-selector/box-selector-item/box-selector-item.html @@ -1,6 +1,20 @@ -
- -
diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html b/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html index 541023c49..bce200564 100644 --- a/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html +++ b/app/portainer/components/forms/git-form/git-form-compose-path-field/git-form-compose-path-field.html @@ -1,11 +1,18 @@
- - Indicate the path to the Compose file from the root of your repository. - + Indicate the path to the {{ $ctrl.deployMethod == 'compose' ? 'Compose' : 'Manifest' }} file from the root of your repository.
- +
- +
diff --git a/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js b/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js index 7906d0e85..03c7b27e1 100644 --- a/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js +++ b/app/portainer/components/forms/git-form/git-form-compose-path-field/index.js @@ -1,6 +1,8 @@ export const gitFormComposePathField = { templateUrl: './git-form-compose-path-field.html', bindings: { + deployMethod: '@', + value: '<', onChange: '<', }, diff --git a/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html b/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html index d8b5908f5..ff5a84f8d 100644 --- a/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html +++ b/app/portainer/components/forms/git-form/git-form-ref-field/git-form-ref-field.html @@ -5,8 +5,8 @@
- -
+ +
diff --git a/app/portainer/components/forms/git-form/git-form.controller.js b/app/portainer/components/forms/git-form/git-form.controller.js index 795e9a7ba..1a0476224 100644 --- a/app/portainer/components/forms/git-form/git-form.controller.js +++ b/app/portainer/components/forms/git-form/git-form.controller.js @@ -15,4 +15,8 @@ export default class GitFormController { }); }; } + + $onInit() { + this.deployMethod = this.deployMethod || 'compose'; + } } diff --git a/app/portainer/components/forms/git-form/git-form.html b/app/portainer/components/forms/git-form/git-form.html index ba35fcf67..7762a0eca 100644 --- a/app/portainer/components/forms/git-form/git-form.html +++ b/app/portainer/components/forms/git-form/git-form.html @@ -1,11 +1,18 @@ -
+
Git repository
- + + - + + + -
+ diff --git a/app/portainer/components/forms/git-form/git-form.js b/app/portainer/components/forms/git-form/git-form.js index affa081dc..8e6bbf70f 100644 --- a/app/portainer/components/forms/git-form/git-form.js +++ b/app/portainer/components/forms/git-form/git-form.js @@ -4,6 +4,7 @@ export const gitForm = { templateUrl: './git-form.html', controller, bindings: { + deployMethod: '@', model: '<', onChange: '<', additionalFile: '<', diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 81340d623..9ab3f4411 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -96,13 +96,13 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.createEdgeStack = function createEdgeStack(stackName, file, edgeGroups) { + service.createEdgeStack = function createEdgeStack({ EdgeGroups, ...payload }, file) { return Upload.upload({ url: 'api/edge_stacks?method=file', data: { - file: file, - Name: stackName, - EdgeGroups: Upload.json(edgeGroups), + file, + EdgeGroups: Upload.json(EdgeGroups), + ...payload, }, ignoreLoadingBar: true, }); diff --git a/app/portainer/services/notifications.js b/app/portainer/services/notifications.js index 2642dcfbf..863f883d7 100644 --- a/app/portainer/services/notifications.js +++ b/app/portainer/services/notifications.js @@ -16,10 +16,10 @@ angular.module('portainer.app').factory('Notifications', [ service.error = function (title, e, fallbackText) { var msg = fallbackText; - if (e.err && e.err.data && e.err.data.message) { - msg = e.err.data.message; - } else if (e.err && e.err.data && e.err.data.details) { + if (e.err && e.err.data && e.err.data.details) { msg = e.err.data.details; + } else if (e.err && e.err.data && e.err.data.message) { + msg = e.err.data.message; } else if (e.data && e.data.details) { msg = e.data.details; } else if (e.data && e.data.message) { @@ -40,6 +40,9 @@ angular.module('portainer.app').factory('Notifications', [ msg = e.msg; } + // eslint-disable-next-line no-console + console.error(e); + if (msg !== 'Invalid JWT token') { toastr.error($sanitize(msg), $sanitize(title), { timeOut: 6000 }); } From 63f64a6a06d9b4ed4f5d5f5a38f2ca1291203f32 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 9 Sep 2021 23:25:55 +0300 Subject: [PATCH 05/26] fix(docker/compose): provide docker config path [EE-1474] (#5468) * fix(docker/compose): provide docker config path * chore(deps): upgrade docker-compose-wrapper --- api/go.mod | 2 +- api/go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 9d36555bd..0cf2de039 100644 --- a/api/go.mod +++ b/api/go.mod @@ -36,7 +36,7 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 github.com/pkg/errors v0.9.1 - github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 + github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 github.com/robfig/cron/v3 v3.0.1 diff --git a/api/go.sum b/api/go.sum index 6aafc2567..18d77a7fd 100644 --- a/api/go.sum +++ b/api/go.sum @@ -206,8 +206,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 h1:5I9j0e6f9KG/RV6YBKWyks8LSHheE+ltJgpMyyWYUoo= -github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= +github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059 h1:98v0k3x3ZXa09NaHP/HmSA83rcN8cuE/zTKo6xvNmoM= +github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= +github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h1:0ZGSu3Atz7RHMDsoITHV676igRfsb51mlgELGo37ELU= +github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= From d166a0951146ca203a2d5e8919c2511299a171a5 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Fri, 10 Sep 2021 02:12:21 +0300 Subject: [PATCH 06/26] fix(backup): backup certs [EE-1479] (#5469) * fix(backup): backup certs * fix(backup): sort files to backup --- api/backup/backup.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/backup/backup.go b/api/backup/backup.go index 0a62786f5..8470f837f 100644 --- a/api/backup/backup.go +++ b/api/backup/backup.go @@ -16,7 +16,18 @@ import ( const rwxr__r__ os.FileMode = 0744 -var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"} +var filesToBackup = []string{ + "certs", + "compose", + "config.json", + "custom_templates", + "edge_jobs", + "edge_stacks", + "extensions", + "portainer.key", + "portainer.pub", + "tls", +} // Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file. func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) { From e86a586651e96fc8dc28b833f0e77bba75459b5d Mon Sep 17 00:00:00 2001 From: Hui Date: Fri, 10 Sep 2021 13:35:37 +1200 Subject: [PATCH 07/26] fix(k8s): manifest file not persisted when deploying with manifest URL EE-1586 --- api/http/handler/stacks/create_kubernetes_stack.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 29ddfe280..08b46069b 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -146,8 +146,6 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit Output: output, } - doCleanUp = false - return response.JSON(w, resp) } @@ -193,11 +191,11 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr doCleanUp := true defer handler.cleanUp(stack, &doCleanUp) - commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword) + commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err} } - stack.GitConfig.ConfigHash = commitId + stack.GitConfig.ConfigHash = commitID stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath) if err != nil { @@ -226,8 +224,6 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr Output: output, } - doCleanUp = false - return response.JSON(w, resp) } @@ -248,6 +244,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit Type: portainer.KubernetesStack, EndpointID: endpoint.ID, EntryPoint: filesystem.ManifestFileDefaultName, + Namespace: payload.Namespace, Status: portainer.StackStatusActive, CreationDate: time.Now().Unix(), CreatedBy: user.Username, @@ -285,6 +282,8 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err} } + doCleanUp = false + resp := &createKubernetesStackResponse{ Output: output, } From 2a60b8fcdf4afbb1e58f6af3a90eb39ae413f78e Mon Sep 17 00:00:00 2001 From: zees-dev <63374656+zees-dev@users.noreply.github.com> Date: Fri, 10 Sep 2021 14:06:57 +1200 Subject: [PATCH 08/26] feat(helm/templates): helm app templates EE-943 (#5449) * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook * feat(helm): list and configure helm chart (#5431) * backport and tidyup code * --amend * using rocket icon for charts * helm chart bugfix - clear category button * added matomo analytics for helm chart install * fix web editor exit warning without changes * editor modified exit bugfix * fixed notifications typo * updated helm template text * helper text to convey slow helm templates load Co-authored-by: zees-dev * removing redundant time-consuming api call by using prop attribute * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * bugfix: kubectl shell not opening - bearer token bug * tidy go modules & remove yarn-error.log * removed redundant handler (not used) - to match EE * resolved merge conflicts, updated code * feat(helm/views): helm release and application views EE-1236 (#5529) * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * removed redundant handler (not used) - to match EE * feat(helm) display helm charts - backend EE-1236 * copy over components for new applications view EE-1236 * Add new applications datatable component * Add more migrated files * removed test not applicable to CE * baclkported EE app data table code to CE * removed redundant helm repo url * resolved conflicts, updated code * using endpoint middleware * PR review fixes * using constants, openapi updated Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: zees-dev * fixed test conflicts, go linted * feat(helm/templates-add): helm templates add repo for user support EE-1278 (#5514) * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev * Pull in all changes from tech review in EE-943 * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * backport EE-1278, squashed, diffed, updated * helm install openapi spec update * resolved conflicts, updated code * - matching ee codebase at 0afe57034449ee0e9f333d92c252a13995a93019 - helm install using endpoint middleware - remove trailing slash from added/persisted helm repo urls * feat(helm) use libhelm url validator and improved path assembly EE-1554 (#5561) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 (#5567) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 * post review changes and further backported changes from EE * resolved conflicts, updated code * fixed helm_install handler unit test * user cannot add existing repo if suffix is '/' (#5571) * feat(helm/docs) fix broken swagger docs EE-1278 (#5572) * Fix swagger docs * minor correction * fix(helm): migrating code from user handler to helm handler (#5573) * - migrated user_helm_repos to helm endpoint handler - migrated api operations from user factory/service to helm factory/service - passing endpointId into helm service/factory as endpoint provider is deprecated * upgrade libhelm to hide secrets Co-authored-by: Matt Hook * removed duplicate file - due to merge conflict * dependency injection in helm factory Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook * kubernetes.templates -> kubernetes.templates.helm name conflict fix * Validate the URL added as a public helm repo (#5579) * fix(helm): helm app deletion fix EE-1581 (#5582) * updated helm lib to show correct error on uninstall failure * passing down helm app namespace on deletion * fix(k8s): EE-1591 non-admin users cannot deploy charts containing secrets (#5590) Co-authored-by: Simon Meng * fix(helm): helm epic bugfixes EE-1582 EE-1593 (#5585) * - trim trailing slash and lowercase before persisting helm repo - browser helm templates url /kubernetes/templates/templates -> /kubernetes/templates/helm - fix publish url - fix helm repo add refresh - semi-fix k8s app expansion * Tidy up swagger documentation related to helm. Make json consistent * fixed helm release page for non-default namespaces * k8s app view table expansion bugfix * EE-1593: publish url load balancer fallback Co-authored-by: Matt Hook * k8s app list fix for charts with deployments containing multiple pods - which use the same label (#5599) * fix(kubernetes): app list view fix for secrets with long keys or values EE-1600 (#5600) * k8s app secrets key value text overflow ellipses * wrapping key value pairs instead of ellipses * fix(helm): helm apps bundling issue across different namespaces EE-1619 (#5602) * helm apps bundling issue across different namespaces * - code comments and indentation to ease reading - moved namespace calc out of loop * feat(helm/test) disable slow helm search test by default EE-1599 (#5598) * skip helm_repo_search as it's an integration test * switch to portainer built in integration test checker * make module order match EE * don't print test struct out when skipping integration test Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook Co-authored-by: cong meng Co-authored-by: Simon Meng --- api/bolt/datastore.go | 57 +++--- .../helmuserrepository/helmuserrepository.go | 73 +++++++ api/bolt/init.go | 1 + api/bolt/migrator/migrate_dbversion31.go | 15 +- api/bolt/services.go | 12 ++ api/cmd/portainer/main.go | 24 ++- api/go.mod | 1 + api/go.sum | 2 + api/http/handler/handler.go | 10 + api/http/handler/helm/handler.go | 103 ++++++++++ api/http/handler/helm/helm_delete.go | 55 ++++++ api/http/handler/helm/helm_delete_test.go | 53 +++++ api/http/handler/helm/helm_install.go | 134 +++++++++++++ api/http/handler/helm/helm_install_test.go | 65 +++++++ api/http/handler/helm/helm_list.go | 63 ++++++ api/http/handler/helm/helm_list_test.go | 60 ++++++ api/http/handler/helm/helm_repo_search.go | 56 ++++++ .../handler/helm/helm_repo_search_test.go | 51 +++++ api/http/handler/helm/helm_show.go | 70 +++++++ api/http/handler/helm/helm_show_test.go | 47 +++++ api/http/handler/helm/user_helm_repos.go | 126 ++++++++++++ .../handler/kubernetes/kubernetes_config.go | 4 +- .../kubernetes/kubernetes_nodes_limits.go | 5 +- api/http/handler/settings/settings_update.go | 16 +- api/http/handler/users/handler.go | 2 +- api/http/proxy/factory/kubernetes/secrets.go | 2 +- api/http/security/bouncer.go | 39 ++-- api/http/security/context.go | 4 +- api/http/server.go | 13 +- api/internal/testhelpers/datastore.go | 88 +++++---- api/internal/testhelpers/integration.go | 22 +++ api/internal/testhelpers/request_bouncer.go | 15 ++ api/jwt/jwt_kubeconfig_test.go | 7 +- api/kubernetes/kubeconfig_service.go | 104 ++++++++++ api/kubernetes/kubeconfig_service_test.go | 149 ++++++++++++++ api/kubernetes/validation/validation.go | 48 +++++ api/portainer.go | 23 +++ app/assets/css/app.css | 9 + app/kubernetes/__module.js | 22 +++ ...plications-datatable-details.controller.js | 12 ++ .../applications-datatable-details.html | 12 ++ .../applications-datatable-details.js | 10 + .../applications-datatable-url.css | 10 + .../applications-datatable-url.html | 6 + .../applications-datatable-url.js | 9 + .../applicationsDatatable.css | 16 ++ .../applicationsDatatable.html | 91 +++++++-- .../applicationsDatatable.js | 3 + .../applicationsDatatableController.js | 90 ++++++++- .../helm-add-repository.controller.js | 40 ++++ .../helm-add-repository.html | 58 ++++++ .../helm-add-repository.js | 11 ++ .../helm-templates-list-item.css | 9 + .../helm-templates-list-item.html | 46 +++++ .../helm-templates-list-item.js | 13 ++ .../helm-templates-list.controller.js | 53 +++++ .../helm-templates-list.html | 57 ++++++ .../helm-templates-list.js | 15 ++ .../helm-templates.controller.js | 183 ++++++++++++++++++ .../helm/helm-templates/helm-templates.html | 182 +++++++++++++++++ .../helm/helm-templates/helm-templates.js | 10 + .../kubernetes-sidebar.html | 4 + app/kubernetes/filters/applicationFilters.js | 2 + app/kubernetes/helm/rest.js | 49 +++++ app/kubernetes/helm/service.js | 102 ++++++++++ app/kubernetes/helpers/application/index.js | 77 +++++++- app/kubernetes/helpers/configurationHelper.js | 14 ++ .../models/application/models/constants.js | 2 + .../models/application/models/index.js | 19 ++ .../views/applications/applications.html | 7 +- .../views/applications/applications.js | 1 + .../applications/applicationsController.js | 47 +++-- .../applications/helm/helm.controller.js | 52 +++++ .../views/applications/helm/helm.css | 5 + .../views/applications/helm/helm.html | 49 +++++ .../views/applications/helm/index.js | 11 ++ .../copy-button/copy-button.controller.js | 13 ++ .../components/copy-button/copy-button.css | 39 ++++ .../components/copy-button/copy-button.html | 4 + .../components/copy-button/copy-button.js | 11 ++ .../sensitive-details/sensitive-details.css | 11 ++ .../sensitive-details/sensitive-details.html | 5 + .../sensitive-details/sensitive-details.js | 10 + .../components/show-hide/show-hide.html | 9 + .../components/show-hide/show-hide.js | 9 + app/portainer/models/settings.js | 1 + app/portainer/views/settings/settings.html | 21 ++ build/download_helm_binary.sh | 17 ++ gruntfile.js | 17 ++ 89 files changed, 3055 insertions(+), 139 deletions(-) create mode 100644 api/bolt/helmuserrepository/helmuserrepository.go create mode 100644 api/http/handler/helm/handler.go create mode 100644 api/http/handler/helm/helm_delete.go create mode 100644 api/http/handler/helm/helm_delete_test.go create mode 100644 api/http/handler/helm/helm_install.go create mode 100644 api/http/handler/helm/helm_install_test.go create mode 100644 api/http/handler/helm/helm_list.go create mode 100644 api/http/handler/helm/helm_list_test.go create mode 100644 api/http/handler/helm/helm_repo_search.go create mode 100644 api/http/handler/helm/helm_repo_search_test.go create mode 100644 api/http/handler/helm/helm_show.go create mode 100644 api/http/handler/helm/helm_show_test.go create mode 100644 api/http/handler/helm/user_helm_repos.go create mode 100644 api/internal/testhelpers/integration.go create mode 100644 api/internal/testhelpers/request_bouncer.go create mode 100644 api/kubernetes/kubeconfig_service.go create mode 100644 api/kubernetes/kubeconfig_service_test.go create mode 100644 api/kubernetes/validation/validation.go create mode 100644 app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js create mode 100644 app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html create mode 100644 app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js create mode 100644 app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css create mode 100644 app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html create mode 100644 app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css create mode 100644 app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.controller.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html create mode 100644 app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.css create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.html create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list-item/helm-templates-list-item.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.controller.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.html create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates-list/helm-templates-list.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates.controller.js create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates.html create mode 100644 app/kubernetes/components/helm/helm-templates/helm-templates.js create mode 100644 app/kubernetes/helm/rest.js create mode 100644 app/kubernetes/helm/service.js create mode 100644 app/kubernetes/views/applications/helm/helm.controller.js create mode 100644 app/kubernetes/views/applications/helm/helm.css create mode 100644 app/kubernetes/views/applications/helm/helm.html create mode 100644 app/kubernetes/views/applications/helm/index.js create mode 100644 app/portainer/components/copy-button/copy-button.controller.js create mode 100644 app/portainer/components/copy-button/copy-button.css create mode 100644 app/portainer/components/copy-button/copy-button.html create mode 100644 app/portainer/components/copy-button/copy-button.js create mode 100644 app/portainer/components/sensitive-details/sensitive-details.css create mode 100644 app/portainer/components/sensitive-details/sensitive-details.html create mode 100644 app/portainer/components/sensitive-details/sensitive-details.js create mode 100644 app/portainer/components/show-hide/show-hide.html create mode 100644 app/portainer/components/show-hide/show-hide.js create mode 100755 build/download_helm_binary.sh diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index c739aa9e6..da8197cd9 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -6,6 +6,8 @@ import ( "path" "time" + "github.com/portainer/portainer/api/bolt/helmuserrepository" + "github.com/boltdb/bolt" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/customtemplate" @@ -44,33 +46,34 @@ const ( // Store defines the implementation of portainer.DataStore using // BoltDB as the storage system. type Store struct { - path string - connection *internal.DbConnection - isNew bool - fileService portainer.FileService - CustomTemplateService *customtemplate.Service - DockerHubService *dockerhub.Service - EdgeGroupService *edgegroup.Service - EdgeJobService *edgejob.Service - EdgeStackService *edgestack.Service - EndpointGroupService *endpointgroup.Service - EndpointService *endpoint.Service - EndpointRelationService *endpointrelation.Service - ExtensionService *extension.Service - RegistryService *registry.Service - ResourceControlService *resourcecontrol.Service - RoleService *role.Service - ScheduleService *schedule.Service - SettingsService *settings.Service - SSLSettingsService *ssl.Service - StackService *stack.Service - TagService *tag.Service - TeamMembershipService *teammembership.Service - TeamService *team.Service - TunnelServerService *tunnelserver.Service - UserService *user.Service - VersionService *version.Service - WebhookService *webhook.Service + path string + connection *internal.DbConnection + isNew bool + fileService portainer.FileService + CustomTemplateService *customtemplate.Service + DockerHubService *dockerhub.Service + EdgeGroupService *edgegroup.Service + EdgeJobService *edgejob.Service + EdgeStackService *edgestack.Service + EndpointGroupService *endpointgroup.Service + EndpointService *endpoint.Service + EndpointRelationService *endpointrelation.Service + ExtensionService *extension.Service + HelmUserRepositoryService *helmuserrepository.Service + RegistryService *registry.Service + ResourceControlService *resourcecontrol.Service + RoleService *role.Service + ScheduleService *schedule.Service + SettingsService *settings.Service + SSLSettingsService *ssl.Service + StackService *stack.Service + TagService *tag.Service + TeamMembershipService *teammembership.Service + TeamService *team.Service + TunnelServerService *tunnelserver.Service + UserService *user.Service + VersionService *version.Service + WebhookService *webhook.Service } func (store *Store) edition() portainer.SoftwareEdition { diff --git a/api/bolt/helmuserrepository/helmuserrepository.go b/api/bolt/helmuserrepository/helmuserrepository.go new file mode 100644 index 000000000..9d5aadb95 --- /dev/null +++ b/api/bolt/helmuserrepository/helmuserrepository.go @@ -0,0 +1,73 @@ +package helmuserrepository + +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "helm_user_repository" +) + +// Service represents a service for managing endpoint data. +type Service struct { + connection *internal.DbConnection +} + +// NewService creates a new instance of a service. +func NewService(connection *internal.DbConnection) (*Service, error) { + err := internal.CreateBucket(connection, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + connection: connection, + }, nil +} + +// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present. +func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) { + var result = make([]portainer.HelmUserRepository, 0) + + err := service.connection.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var record portainer.HelmUserRepository + err := internal.UnmarshalObject(v, &record) + if err != nil { + return err + } + + if record.UserID == userID { + result = append(result, record) + } + } + + return nil + }) + + return result, err +} + +// CreateHelmUserRepository creates a new HelmUserRepository object. +func (service *Service) CreateHelmUserRepository(record *portainer.HelmUserRepository) error { + return service.connection.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + id, _ := bucket.NextSequence() + record.ID = portainer.HelmUserRepositoryID(id) + + data, err := internal.MarshalObject(record) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(record.ID)), data) + }) +} diff --git a/api/bolt/init.go b/api/bolt/init.go index 4df6b327b..4b9e4559f 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -44,6 +44,7 @@ func (store *Store) Init() error { EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, TemplatesURL: portainer.DefaultTemplatesURL, + HelmRepositoryURL: portainer.DefaultHelmRepositoryURL, UserSessionTimeout: portainer.DefaultUserSessionTimeout, KubeconfigExpiry: portainer.DefaultKubeconfigExpiry, } diff --git a/api/bolt/migrator/migrate_dbversion31.go b/api/bolt/migrator/migrate_dbversion31.go index e9a242f70..c2c17eca5 100644 --- a/api/bolt/migrator/migrate_dbversion31.go +++ b/api/bolt/migrator/migrate_dbversion31.go @@ -28,6 +28,10 @@ func (m *Migrator) migrateDBVersionToDB32() error { return err } + if err := m.helmRepositoryURLToDB32(); err != nil { + return err + } + return nil } @@ -223,4 +227,13 @@ func (m *Migrator) kubeconfigExpiryToDB32() error { } settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry return m.settingsService.UpdateSettings(settings) -} \ No newline at end of file +} + +func (m *Migrator) helmRepositoryURLToDB32() error { + settings, err := m.settingsService.Settings() + if err != nil { + return err + } + settings.HelmRepositoryURL = portainer.DefaultHelmRepositoryURL + return m.settingsService.UpdateSettings(settings) +} diff --git a/api/bolt/services.go b/api/bolt/services.go index 54b1bd9ce..2ee5264bc 100644 --- a/api/bolt/services.go +++ b/api/bolt/services.go @@ -11,6 +11,7 @@ import ( "github.com/portainer/portainer/api/bolt/endpointgroup" "github.com/portainer/portainer/api/bolt/endpointrelation" "github.com/portainer/portainer/api/bolt/extension" + "github.com/portainer/portainer/api/bolt/helmuserrepository" "github.com/portainer/portainer/api/bolt/registry" "github.com/portainer/portainer/api/bolt/resourcecontrol" "github.com/portainer/portainer/api/bolt/role" @@ -88,6 +89,12 @@ func (store *Store) initServices() error { } store.ExtensionService = extensionService + helmUserRepositoryService, err := helmuserrepository.NewService(store.connection) + if err != nil { + return err + } + store.HelmUserRepositoryService = helmUserRepositoryService + registryService, err := registry.NewService(store.connection) if err != nil { return err @@ -204,6 +211,11 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService { return store.EndpointRelationService } +// HelmUserRepository access the helm user repository settings +func (store *Store) HelmUserRepository() portainer.HelmUserRepositoryService { + return store.HelmUserRepositoryService +} + // Registry gives access to the Registry data management layer func (store *Store) Registry() portainer.RegistryService { return store.RegistryService diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 00080dade..9eeb62e61 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -13,6 +13,7 @@ import ( "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/docker" + "github.com/portainer/libhelm" "github.com/portainer/portainer/api/exec" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/git" @@ -102,6 +103,10 @@ func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheMan return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath) } +func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) { + return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath}) +} + func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) { settings, err := dataStore.Settings().Settings() if err != nil { @@ -420,6 +425,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { log.Fatal(err) } + sslSettings, err := sslService.GetSSLSettings() + if err != nil { + log.Fatalf("failed to get ssl settings: %s", err) + } + err = initKeyPair(fileService, digitalSignatureService) if err != nil { log.Fatalf("failed initializing key pai: %v", err) @@ -445,6 +455,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { authorizationService.K8sClientFactory = kubernetesClientFactory kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager() + + kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath) + proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager) dockerConfigPath := fileService.GetDockerConfigPath() @@ -458,6 +471,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets) + helmPackageManager, err := initHelmPackageManager(*flags.Assets) + if err != nil { + log.Fatalf("failed initializing helm package manager: %s", err) + } + if dataStore.IsNew() { err = updateSettingsFromFlags(dataStore, flags) if err != nil { @@ -518,7 +536,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { log.Fatalf("failed starting tunnel server: %s", err) } - sslSettings, err := dataStore.SSLSettings().Settings() + sslDBSettings, err := dataStore.SSLSettings().Settings() if err != nil { log.Fatalf("failed to fetch ssl settings from DB") } @@ -533,12 +551,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { Status: applicationStatus, BindAddress: *flags.Addr, BindAddressHTTPS: *flags.AddrHTTPS, - HTTPEnabled: sslSettings.HTTPEnabled, + HTTPEnabled: sslDBSettings.HTTPEnabled, AssetsPath: *flags.Assets, DataStore: dataStore, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, KubernetesDeployer: kubernetesDeployer, + HelmPackageManager: helmPackageManager, CryptoService: cryptoService, JWTService: jwtService, FileService: fileService, @@ -547,6 +566,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { GitService: gitService, ProxyManager: proxyManager, KubernetesTokenCacheManager: kubernetesTokenCacheManager, + KubeConfigService: kubeConfigService, SignatureService: digitalSignatureService, SnapshotService: snapshotService, SSLService: sslService, diff --git a/api/go.mod b/api/go.mod index 0cf2de039..39f55db29 100644 --- a/api/go.mod +++ b/api/go.mod @@ -38,6 +38,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a + github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97 github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 diff --git a/api/go.sum b/api/go.sum index 18d77a7fd..cffccad43 100644 --- a/api/go.sum +++ b/api/go.sum @@ -212,6 +212,8 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE= +github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97 h1:ZcRVgWHTac8V7WU9TUBr73H3e5ajVFYTPjPl9TWULDA= +github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index e1a80581c..dbd2c6c20 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -16,6 +16,7 @@ import ( "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/helm" "github.com/portainer/portainer/api/http/handler/kubernetes" "github.com/portainer/portainer/api/http/handler/motd" "github.com/portainer/portainer/api/http/handler/registries" @@ -47,7 +48,9 @@ type Handler struct { EndpointEdgeHandler *endpointedge.Handler EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler + EndpointHelmHandler *helm.Handler EndpointProxyHandler *endpointproxy.Handler + HelmTemplatesHandler *helm.Handler KubernetesHandler *kubernetes.Handler FileHandler *file.Handler MOTDHandler *motd.Handler @@ -166,6 +169,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/kubernetes"): http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r) + + // Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm + case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"): + http.StripPrefix("/api/endpoints", h.EndpointHelmHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/endpoints"): switch { case strings.Contains(r.URL.Path, "/docker/"): @@ -199,6 +207,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/tags"): http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/templates/helm"): + http.StripPrefix("/api", h.HelmTemplatesHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/templates"): http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/upload"): diff --git a/api/http/handler/helm/handler.go b/api/http/handler/helm/handler.go new file mode 100644 index 000000000..106992e20 --- /dev/null +++ b/api/http/handler/helm/handler.go @@ -0,0 +1,103 @@ +package helm + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/portainer/libhelm" + "github.com/portainer/libhelm/options" + httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/middlewares" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes" +) + +const ( + handlerActivityContext = "Kubernetes" +) + +type requestBouncer interface { + AuthenticatedAccess(h http.Handler) http.Handler +} + +// Handler is the HTTP handler used to handle endpoint group operations. +type Handler struct { + *mux.Router + requestBouncer requestBouncer + dataStore portainer.DataStore + kubeConfigService kubernetes.KubeConfigService + helmPackageManager libhelm.HelmPackageManager +} + +// NewHandler creates a handler to manage endpoint group operations. +func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + requestBouncer: bouncer, + dataStore: dataStore, + helmPackageManager: helmPackageManager, + kubeConfigService: kubeConfigService, + } + + h.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id")) + + // `helm list -o json` + h.Handle("/{id}/kubernetes/helm", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmList))).Methods(http.MethodGet) + + // `helm delete RELEASE_NAME` + h.Handle("/{id}/kubernetes/helm/{release}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmDelete))).Methods(http.MethodDelete) + + // `helm install [NAME] [CHART] flags` + h.Handle("/{id}/kubernetes/helm", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmInstall))).Methods(http.MethodPost) + + h.Handle("/{id}/kubernetes/helm/repositories", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userGetHelmRepos))).Methods(http.MethodGet) + h.Handle("/{id}/kubernetes/helm/repositories", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userCreateHelmRepo))).Methods(http.MethodPost) + + return h +} + +// NewTemplateHandler creates a template handler to manage endpoint group operations. +func NewTemplateHandler(bouncer requestBouncer, helmPackageManager libhelm.HelmPackageManager) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + helmPackageManager: helmPackageManager, + requestBouncer: bouncer, + } + + h.Handle("/templates/helm", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmRepoSearch))).Methods(http.MethodGet) + + // helm show [COMMAND] [CHART] [REPO] flags + h.Handle("/templates/helm/{command:chart|values|readme}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmShow))).Methods(http.MethodGet) + + return h +} + +// getHelmClusterAccess obtains the core k8s cluster access details from request. +// The cluster access includes the cluster server url, the user's bearer token and the tls certificate. +// The cluster access is passed in as kube config CLI params to helm binary. +func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.KubernetesClusterAccess, *httperror.HandlerError) { + endpoint, err := middlewares.FetchEndpoint(r) + if err != nil { + return nil, &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err} + } + + bearerToken, err := security.ExtractBearerToken(r) + if err != nil { + return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err} + } + + kubeConfigInternal := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken) + return &options.KubernetesClusterAccess{ + ClusterServerURL: kubeConfigInternal.ClusterServerURL, + CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile, + AuthToken: kubeConfigInternal.AuthToken, + }, nil +} diff --git a/api/http/handler/helm/helm_delete.go b/api/http/handler/helm/helm_delete.go new file mode 100644 index 000000000..7939c253f --- /dev/null +++ b/api/http/handler/helm/helm_delete.go @@ -0,0 +1,55 @@ +package helm + +import ( + "net/http" + + "github.com/portainer/libhelm/options" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" +) + +// @id HelmDelete +// @summary Delete Helm Release +// @description +// @description **Access policy**: authorized +// @tags helm +// @security jwt +// @accept json +// @produce json +// @param release query string true "The name of the release/application to uninstall" +// @param namespace query string true "An optional namespace" +// @success 204 "Success" +// @failure 400 "Invalid endpoint id or bad request" +// @failure 401 "Unauthorized" +// @failure 404 "Endpoint or ServiceAccount not found" +// @failure 500 "Server error or helm error" +// @router /endpoints/:id/kubernetes/helm/{release} [delete] +func (handler *Handler) helmDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + release, err := request.RetrieveRouteVariableValue(r, "release") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "No release specified", err} + } + + clusterAccess, httperr := handler.getHelmClusterAccess(r) + if httperr != nil { + return httperr + } + + uninstallOpts := options.UninstallOptions{ + Name: release, + KubernetesClusterAccess: clusterAccess, + } + + q := r.URL.Query() + if namespace := q.Get("namespace"); namespace != "" { + uninstallOpts.Namespace = namespace + } + + err = handler.helmPackageManager.Uninstall(uninstallOpts) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/helm/helm_delete_test.go b/api/http/handler/helm/helm_delete_test.go new file mode 100644 index 000000000..5b7babc61 --- /dev/null +++ b/api/http/handler/helm/helm_delete_test.go @@ -0,0 +1,53 @@ +package helm + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/portainer/libhelm/binary/test" + "github.com/portainer/libhelm/options" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes" + "github.com/stretchr/testify/assert" + + bolt "github.com/portainer/portainer/api/bolt/bolttest" + helper "github.com/portainer/portainer/api/internal/testhelpers" +) + +func Test_helmDelete(t *testing.T) { + is := assert.New(t) + + store, teardown := bolt.MustNewTestStore(true) + defer teardown() + + err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1}) + is.NoError(err, "Error creating endpoint") + + err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) + is.NoError(err, "Error creating a user") + + helmPackageManager := test.NewMockHelmBinaryPackageManager("") + kubeConfigService := kubernetes.NewKubeConfigCAService("", "") + h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + + is.NotNil(h, "Handler should not fail") + + // Install a single chart directly, to be deleted by the handler + options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"} + h.helmPackageManager.Install(options) + + t.Run("helmDelete succeeds with admin user", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/1/kubernetes/helm/%s", options.Name), nil) + ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1}) + req = req.WithContext(ctx) + req.Header.Add("Authorization", "Bearer dummytoken") + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(http.StatusNoContent, rr.Code, "Status should be 204") + }) +} diff --git a/api/http/handler/helm/helm_install.go b/api/http/handler/helm/helm_install.go new file mode 100644 index 000000000..33860ad7f --- /dev/null +++ b/api/http/handler/helm/helm_install.go @@ -0,0 +1,134 @@ +package helm + +import ( + "errors" + "fmt" + "net/http" + "os" + "strings" + + "github.com/portainer/libhelm/options" + "github.com/portainer/libhelm/release" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api/kubernetes/validation" +) + +type installChartPayload struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Chart string `json:"chart"` + Repo string `json:"repo"` + Values string `json:"values"` +} + +var errChartNameInvalid = errors.New("invalid chart name. " + + "Chart name must consist of lower case alphanumeric characters, '-' or '.'," + + " and must start and end with an alphanumeric character", +) + +// @id HelmInstall +// @summary Install Helm Chart +// @description +// @description **Access policy**: authorized +// @tags helm +// @security jwt +// @accept json +// @produce json +// @param payload body installChartPayload true "Chart details" +// @success 201 {object} release.Release "Created" +// @failure 401 "Unauthorized" +// @failure 404 "Endpoint or ServiceAccount not found" +// @failure 500 "Server error" +// @router /endpoints/:id/kubernetes/helm [post] +func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload installChartPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{ + StatusCode: http.StatusBadRequest, + Message: "Invalid Helm install payload", + Err: err, + } + } + + release, err := handler.installChart(r, payload) + if err != nil { + return &httperror.HandlerError{ + StatusCode: http.StatusInternalServerError, + Message: "Unable to install a chart", + Err: err, + } + } + + w.WriteHeader(http.StatusCreated) + return response.JSON(w, release) +} + +func (p *installChartPayload) Validate(_ *http.Request) error { + var required []string + if p.Repo == "" { + required = append(required, "repo") + } + if p.Name == "" { + required = append(required, "name") + } + if p.Namespace == "" { + required = append(required, "namespace") + } + if p.Chart == "" { + required = append(required, "chart") + } + if len(required) > 0 { + return fmt.Errorf("required field(s) missing: %s", strings.Join(required, ", ")) + } + + if errs := validation.IsDNS1123Subdomain(p.Name); len(errs) > 0 { + return errChartNameInvalid + } + + return nil +} + +func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) { + clusterAccess, httperr := handler.getHelmClusterAccess(r) + if httperr != nil { + return nil, httperr.Err + } + installOpts := options.InstallOptions{ + Name: p.Name, + Chart: p.Chart, + Namespace: p.Namespace, + Repo: p.Repo, + KubernetesClusterAccess: &options.KubernetesClusterAccess{ + ClusterServerURL: clusterAccess.ClusterServerURL, + CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile, + AuthToken: clusterAccess.AuthToken, + }, + } + + if p.Values != "" { + file, err := os.CreateTemp("", "helm-values") + if err != nil { + return nil, err + } + defer os.Remove(file.Name()) + _, err = file.WriteString(p.Values) + if err != nil { + file.Close() + return nil, err + } + err = file.Close() + if err != nil { + return nil, err + } + installOpts.ValuesFile = file.Name() + } + + release, err := handler.helmPackageManager.Install(installOpts) + if err != nil { + return nil, err + } + return release, nil +} diff --git a/api/http/handler/helm/helm_install_test.go b/api/http/handler/helm/helm_install_test.go new file mode 100644 index 000000000..f671a067f --- /dev/null +++ b/api/http/handler/helm/helm_install_test.go @@ -0,0 +1,65 @@ +package helm + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/portainer/libhelm/binary/test" + "github.com/portainer/libhelm/options" + "github.com/portainer/libhelm/release" + portainer "github.com/portainer/portainer/api" + bolt "github.com/portainer/portainer/api/bolt/bolttest" + "github.com/portainer/portainer/api/http/security" + helper "github.com/portainer/portainer/api/internal/testhelpers" + "github.com/portainer/portainer/api/kubernetes" + "github.com/stretchr/testify/assert" +) + +func Test_helmInstall(t *testing.T) { + is := assert.New(t) + + store, teardown := bolt.MustNewTestStore(true) + defer teardown() + + err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1}) + is.NoError(err, "error creating endpoint") + + err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) + is.NoError(err, "error creating a user") + + helmPackageManager := test.NewMockHelmBinaryPackageManager("") + kubeConfigService := kubernetes.NewKubeConfigCAService("", "") + h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + + 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"} + optdata, err := json.Marshal(options) + is.NoError(err) + + t.Run("helmInstall succeeds with admin user", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/1/kubernetes/helm", bytes.NewBuffer(optdata)) + ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1}) + req = req.WithContext(ctx) + req.Header.Add("Authorization", "Bearer dummytoken") + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(http.StatusCreated, rr.Code, "Status should be 201") + + body, err := io.ReadAll(rr.Body) + is.NoError(err, "ReadAll should not return error") + + resp := release.Release{} + err = json.Unmarshal(body, &resp) + is.NoError(err, "response should be json") + is.EqualValues(options.Name, resp.Name, "Name doesn't match") + is.EqualValues(options.Namespace, resp.Namespace, "Namespace doesn't match") + }) +} diff --git a/api/http/handler/helm/helm_list.go b/api/http/handler/helm/helm_list.go new file mode 100644 index 000000000..f943a31ef --- /dev/null +++ b/api/http/handler/helm/helm_list.go @@ -0,0 +1,63 @@ +package helm + +import ( + "net/http" + + "github.com/portainer/libhelm/options" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" +) + +// @id HelmList +// @summary List Helm Releases +// @description +// @description **Access policy**: authorized +// @tags helm +// @security jwt +// @accept json +// @produce json +// @param namespace query string true "specify an optional namespace" +// @param filter query string true "specify an optional filter" +// @param selector query string true "specify an optional selector" +// @success 200 {array} release.ReleaseElement "Success" +// @failure 400 "Invalid endpoint identifier" +// @failure 401 "Unauthorized" +// @failure 404 "Endpoint or ServiceAccount not found" +// @failure 500 "Server error" +// @router /endpoints/:id/kubernetes/helm [get] +func (handler *Handler) helmList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + clusterAccess, httperr := handler.getHelmClusterAccess(r) + if httperr != nil { + return httperr + } + + listOpts := options.ListOptions{ + KubernetesClusterAccess: clusterAccess, + } + + params := r.URL.Query() + + // optional namespace. The library defaults to "default" + namespace, _ := request.RetrieveQueryParameter(r, "namespace", true) + if namespace != "" { + listOpts.Namespace = namespace + } + + // optional filter + if filter := params.Get("filter"); filter != "" { + listOpts.Filter = filter + } + + // optional selector + if selector := params.Get("selector"); selector != "" { + listOpts.Selector = selector + } + + releases, err := handler.helmPackageManager.List(listOpts) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err} + } + + return response.JSON(w, releases) +} diff --git a/api/http/handler/helm/helm_list_test.go b/api/http/handler/helm/helm_list_test.go new file mode 100644 index 000000000..9aa7d29d3 --- /dev/null +++ b/api/http/handler/helm/helm_list_test.go @@ -0,0 +1,60 @@ +package helm + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/portainer/libhelm/binary/test" + "github.com/portainer/libhelm/options" + "github.com/portainer/libhelm/release" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/kubernetes" + "github.com/stretchr/testify/assert" + + bolt "github.com/portainer/portainer/api/bolt/bolttest" + helper "github.com/portainer/portainer/api/internal/testhelpers" +) + +func Test_helmList(t *testing.T) { + store, teardown := bolt.MustNewTestStore(true) + defer teardown() + + err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1}) + assert.NoError(t, err, "error creating endpoint") + + err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) + assert.NoError(t, err, "error creating a user") + + helmPackageManager := test.NewMockHelmBinaryPackageManager("") + kubeConfigService := kubernetes.NewKubeConfigCAService("", "") + h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + + // Install a single chart. We expect to get these values back + options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"} + h.helmPackageManager.Install(options) + + t.Run("helmList", func(t *testing.T) { + is := assert.New(t) + + req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil) + req.Header.Add("Authorization", "Bearer dummytoken") + + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK") + + body, err := io.ReadAll(rr.Body) + is.NoError(err, "ReadAll should not return error") + + data := []release.ReleaseElement{} + json.Unmarshal(body, &data) + if is.Equal(1, len(data), "Expected one chart entry") { + is.EqualValues(options.Name, data[0].Name, "Name doesn't match") + is.EqualValues(options.Chart, data[0].Chart, "Chart doesn't match") + } + }) +} diff --git a/api/http/handler/helm/helm_repo_search.go b/api/http/handler/helm/helm_repo_search.go new file mode 100644 index 000000000..499255bf9 --- /dev/null +++ b/api/http/handler/helm/helm_repo_search.go @@ -0,0 +1,56 @@ +package helm + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/pkg/errors" + "github.com/portainer/libhelm" + "github.com/portainer/libhelm/options" + httperror "github.com/portainer/libhttp/error" +) + +// @id HelmRepoSearch +// @summary Search Helm Charts +// @description +// @description **Access policy**: authorized +// @tags helm +// @param repo query string true "Helm repository URL" +// @security jwt +// @produce json +// @success 200 {object} string "Success" +// @failure 400 "Bad request" +// @failure 401 "Unauthorized" +// @failure 404 "Not found" +// @failure 500 "Server error" +// @router /templates/helm [get] +func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + repo := r.URL.Query().Get("repo") + if repo == "" { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")} + } + + _, err := url.ParseRequestURI(repo) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))} + } + + searchOpts := options.SearchRepoOptions{ + Repo: repo, + } + + result, err := libhelm.SearchRepo(searchOpts) + if err != nil { + return &httperror.HandlerError{ + StatusCode: http.StatusInternalServerError, + Message: "Search failed", + Err: err, + } + } + + w.Header().Set("Content-Type", "text/plain") + w.Write(result) + + return nil +} diff --git a/api/http/handler/helm/helm_repo_search_test.go b/api/http/handler/helm/helm_repo_search_test.go new file mode 100644 index 000000000..beea99f92 --- /dev/null +++ b/api/http/handler/helm/helm_repo_search_test.go @@ -0,0 +1,51 @@ +package helm + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/portainer/libhelm/binary/test" + "github.com/stretchr/testify/assert" + + helper "github.com/portainer/portainer/api/internal/testhelpers" +) + +func Test_helmRepoSearch(t *testing.T) { + helper.IntegrationTest(t) + is := assert.New(t) + + helmPackageManager := test.NewMockHelmBinaryPackageManager("") + h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager) + + assert.NotNil(t, h, "Handler should not fail") + + repos := []string{"https://charts.bitnami.com/bitnami", "https://portainer.github.io/k8s"} + + for _, repo := range repos { + t.Run(repo, func(t *testing.T) { + repoUrlEncoded := url.QueryEscape(repo) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK") + body, err := io.ReadAll(rr.Body) + is.NoError(err, "ReadAll should not return error") + is.NotEmpty(body, "Body should not be empty") + }) + } + + t.Run("fails on invalid URL", func(t *testing.T) { + repo := "abc.com" + repoUrlEncoded := url.QueryEscape(repo) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(http.StatusBadRequest, rr.Code, "Status should be 400 Bad request") + }) +} diff --git a/api/http/handler/helm/helm_show.go b/api/http/handler/helm/helm_show.go new file mode 100644 index 000000000..220e33ab4 --- /dev/null +++ b/api/http/handler/helm/helm_show.go @@ -0,0 +1,70 @@ +package helm + +import ( + "fmt" + "log" + "net/http" + "net/url" + + "github.com/pkg/errors" + "github.com/portainer/libhelm/options" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" +) + +// @id HelmShow +// @summary Show Helm Chart Information +// @description +// @description **Access policy**: authorized +// @tags helm_chart +// @param repo query string true "Helm repository URL" +// @param chart query string true "Chart name" +// @param command path string false "chart/values/readme" +// @security jwt +// @accept json +// @produce text/plain +// @success 200 {object} string "Success" +// @failure 401 "Unauthorized" +// @failure 404 "Endpoint or ServiceAccount not found" +// @failure 500 "Server error" +// @router /templates/helm/{command} [get] +func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + repo := r.URL.Query().Get("repo") + if repo == "" { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")} + } + _, err := url.ParseRequestURI(repo) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))} + } + + chart := r.URL.Query().Get("chart") + if chart == "" { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `chart` query parameter")} + } + + cmd, err := request.RetrieveRouteVariableValue(r, "command") + if err != nil { + cmd = "all" + log.Printf("[DEBUG] [internal,helm] [message: command not provided, defaulting to %s]", cmd) + } + + showOptions := options.ShowOptions{ + OutputFormat: options.ShowOutputFormat(cmd), + Chart: chart, + Repo: repo, + } + result, err := handler.helmPackageManager.Show(showOptions) + if err != nil { + return &httperror.HandlerError{ + StatusCode: http.StatusInternalServerError, + Message: "Unable to show chart", + Err: err, + } + } + + w.Header().Set("Content-Type", "text/plain") + w.Write(result) + + return nil +} diff --git a/api/http/handler/helm/helm_show_test.go b/api/http/handler/helm/helm_show_test.go new file mode 100644 index 000000000..c619dd01e --- /dev/null +++ b/api/http/handler/helm/helm_show_test.go @@ -0,0 +1,47 @@ +package helm + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/portainer/libhelm/binary/test" + helper "github.com/portainer/portainer/api/internal/testhelpers" + "github.com/stretchr/testify/assert" +) + +func Test_helmShow(t *testing.T) { + is := assert.New(t) + + helmPackageManager := test.NewMockHelmBinaryPackageManager("") + h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager) + + is.NotNil(h, "Handler should not fail") + + commands := map[string]string{ + "values": test.MockDataValues, + "chart": test.MockDataChart, + "readme": test.MockDataReadme, + } + + for cmd, expect := range commands { + t.Run(cmd, func(t *testing.T) { + is.NotNil(h, "Handler should not fail") + + repoUrlEncoded := url.QueryEscape("https://charts.bitnami.com/bitnami") + chart := "nginx" + req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s?repo=%s&chart=%s", cmd, repoUrlEncoded, chart), nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + is.Equal(rr.Code, http.StatusOK, "Status should be 200 OK") + + body, err := io.ReadAll(rr.Body) + is.NoError(err, "ReadAll should not return error") + is.EqualValues(string(body), expect, "Unexpected search response") + }) + } +} diff --git a/api/http/handler/helm/user_helm_repos.go b/api/http/handler/helm/user_helm_repos.go new file mode 100644 index 000000000..31df98d78 --- /dev/null +++ b/api/http/handler/helm/user_helm_repos.go @@ -0,0 +1,126 @@ +package helm + +import ( + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/portainer/libhelm" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +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) +} + +// @id HelmUserRepositoryCreate +// @summary Create a user helm repository +// @description Create a user helm repository. +// @description **Access policy**: authenticated +// @tags helm +// @security jwt +// @accept json +// @produce json +// @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" +// @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.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err} + } + userID := portainer.UserID(tokenData.ID) + + p := new(addHelmRepoUrlPayload) + err = request.DecodeAndValidateJSONPayload(r, p) + if err != nil { + return &httperror.HandlerError{ + StatusCode: http.StatusBadRequest, + Message: "Invalid Helm repository URL", + Err: 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.HandlerError{http.StatusInternalServerError, "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.HandlerError{StatusCode: http.StatusBadRequest, Message: errMsg, Err: errors.New(errMsg)} + } + } + + record := portainer.HelmUserRepository{ + UserID: userID, + URL: p.URL, + } + + err = handler.dataStore.HelmUserRepository().CreateHelmUserRepository(&record) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save a user Helm repository URL", err} + } + + return response.JSON(w, record) +} + +// @id HelmUserRepositoriesList +// @summary List a users helm repositories +// @description Inspect a user helm repositories. +// @description **Access policy**: authenticated +// @tags helm +// @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" +// @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.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err} + } + userID := portainer.UserID(tokenData.ID) + + settings, err := handler.dataStore.Settings().Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + userRepos, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "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/kubernetes/kubernetes_config.go b/api/http/handler/kubernetes/kubernetes_config.go index 898aef0a4..a3a545753 100644 --- a/api/http/handler/kubernetes/kubernetes_config.go +++ b/api/http/handler/kubernetes/kubernetes_config.go @@ -3,6 +3,8 @@ package kubernetes import ( "errors" "fmt" + "net/http" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" @@ -10,8 +12,6 @@ import ( bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" kcli "github.com/portainer/portainer/api/kubernetes/cli" - - "net/http" ) // @id GetKubernetesConfig diff --git a/api/http/handler/kubernetes/kubernetes_nodes_limits.go b/api/http/handler/kubernetes/kubernetes_nodes_limits.go index 18a46d05c..09a64b116 100644 --- a/api/http/handler/kubernetes/kubernetes_nodes_limits.go +++ b/api/http/handler/kubernetes/kubernetes_nodes_limits.go @@ -1,12 +1,13 @@ package kubernetes import ( + "net/http" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" - "net/http" ) // @id getKubernetesNodesLimits @@ -18,7 +19,7 @@ import ( // @accept json // @produce json // @param id path int true "Endpoint identifier" -// @success 200 {object} K8sNodesLimits "Success" +// @success 200 {object} portainer.K8sNodesLimits "Success" // @failure 400 "Invalid request" // @failure 401 "Unauthorized" // @failure 403 "Permission denied" diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 7258170d9..aa02524ab 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -1,11 +1,13 @@ package settings import ( - "errors" "net/http" + "strings" "time" "github.com/asaskevich/govalidator" + "github.com/pkg/errors" + "github.com/portainer/libhelm" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" @@ -36,6 +38,8 @@ type settingsUpdatePayload struct { KubeconfigExpiry *string `example:"24h" default:"0"` // Whether telemetry is enabled EnableTelemetry *bool `example:"false"` + // Helm repository URL + HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"` } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -48,6 +52,12 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error { if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) { return errors.New("Invalid external templates URL. Must correspond to a valid URL format") } + if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" { + err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL) + if err != nil { + return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format") + } + } if payload.UserSessionTimeout != nil { _, err := time.ParseDuration(*payload.UserSessionTimeout) if err != nil { @@ -101,6 +111,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.TemplatesURL = *payload.TemplatesURL } + if payload.HelmRepositoryURL != nil { + settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/") + } + if payload.BlackListedLabels != nil { settings.BlackListedLabels = payload.BlackListedLabels } diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index 5ada9b87a..f6c399c7e 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -4,7 +4,7 @@ import ( "errors" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "net/http" diff --git a/api/http/proxy/factory/kubernetes/secrets.go b/api/http/proxy/factory/kubernetes/secrets.go index 6fc2ed7f6..dc03007d9 100644 --- a/api/http/proxy/factory/kubernetes/secrets.go +++ b/api/http/proxy/factory/kubernetes/secrets.go @@ -158,7 +158,7 @@ func (transport *baseTransport) proxySecretDeleteOperation(request *http.Request } func isSecretRepresentPrivateRegistry(secret map[string]interface{}) bool { - if secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) { + if secret["type"] == nil || secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) { return false } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 692c49544..ace0197f8 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -199,25 +199,14 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tokenData *portainer.TokenData - var token string - // Optionally, token might be set via the "token" query parameter. - // For example, in websocket requests - token = r.URL.Query().Get("token") - - // Get token from the Authorization header - tokens, ok := r.Header["Authorization"] - if ok && len(tokens) >= 1 { - token = tokens[0] - token = strings.TrimPrefix(token, "Bearer ") - } - - if token == "" { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized) + // get token from the Authorization header or query parameter + token, err := ExtractBearerToken(r) + if err != nil { + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", err) return } - var err error tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token) if err != nil { httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", err) @@ -233,12 +222,28 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han return } - ctx := storeTokenData(r, tokenData) + ctx := StoreTokenData(r, tokenData) next.ServeHTTP(w, r.WithContext(ctx)) - return }) } +// ExtractBearerToken extracts the Bearer token from the request header or query parameter and returns the token. +func ExtractBearerToken(r *http.Request) (string, error) { + // Optionally, token might be set via the "token" query parameter. + // For example, in websocket requests + token := r.URL.Query().Get("token") + + tokens, ok := r.Header["Authorization"] + if ok && len(tokens) >= 1 { + token = tokens[0] + token = strings.TrimPrefix(token, "Bearer ") + } + if token == "" { + return "", httperrors.ErrUnauthorized + } + return token, nil +} + // mwSecureHeaders provides secure headers middleware for handlers. func mwSecureHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/api/http/security/context.go b/api/http/security/context.go index 1601f61ac..5e96b5b88 100644 --- a/api/http/security/context.go +++ b/api/http/security/context.go @@ -17,8 +17,8 @@ const ( contextRestrictedRequest ) -// storeTokenData stores a TokenData object inside the request context and returns the enhanced context. -func storeTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context { +// StoreTokenData stores a TokenData object inside the request context and returns the enhanced context. +func StoreTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context { return context.WithValue(request.Context(), contextAuthenticationKey, tokenData) } diff --git a/api/http/server.go b/api/http/server.go index d826760f0..37df3cc78 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "github.com/portainer/libhelm" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/adminmonitor" "github.com/portainer/portainer/api/crypto" @@ -26,6 +27,7 @@ import ( "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/helm" kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes" "github.com/portainer/portainer/api/http/handler/motd" "github.com/portainer/portainer/api/http/handler/registries" @@ -49,6 +51,7 @@ import ( "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/ssl" + k8s "github.com/portainer/portainer/api/kubernetes" "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/scheduler" stackdeployer "github.com/portainer/portainer/api/stacks" @@ -76,11 +79,13 @@ type Server struct { SwarmStackManager portainer.SwarmStackManager ProxyManager *proxy.Manager KubernetesTokenCacheManager *kubernetes.TokenCacheManager + KubeConfigService k8s.KubeConfigService Handler *handler.Handler SSLService *ssl.Service DockerClientFactory *docker.ClientFactory KubernetesClientFactory *cli.ClientFactory KubernetesDeployer portainer.KubernetesDeployer + HelmPackageManager libhelm.HelmPackageManager Scheduler *scheduler.Scheduler ShutdownCtx context.Context ShutdownTrigger context.CancelFunc @@ -166,6 +171,10 @@ func (server *Server) Start() error { var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) + var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService) + + var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager) + var motdHandler = motd.NewHandler(requestBouncer) var registryHandler = registries.NewHandler(requestBouncer) @@ -242,10 +251,12 @@ func (server *Server) Start() error { EdgeTemplatesHandler: edgeTemplatesHandler, EndpointGroupHandler: endpointGroupHandler, EndpointHandler: endpointHandler, + EndpointHelmHandler: endpointHelmHandler, EndpointEdgeHandler: endpointEdgeHandler, EndpointProxyHandler: endpointProxyHandler, - KubernetesHandler: kubernetesHandler, FileHandler: fileHandler, + HelmTemplatesHandler: helmTemplatesHandler, + KubernetesHandler: kubernetesHandler, MOTDHandler: motdHandler, RegistryHandler: registryHandler, ResourceControlHandler: resourceControlHandler, diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go index 89e3b0329..c934691c2 100644 --- a/api/internal/testhelpers/datastore.go +++ b/api/internal/testhelpers/datastore.go @@ -8,26 +8,27 @@ import ( ) type datastore struct { - customTemplate portainer.CustomTemplateService - edgeGroup portainer.EdgeGroupService - edgeJob portainer.EdgeJobService - edgeStack portainer.EdgeStackService - endpoint portainer.EndpointService - endpointGroup portainer.EndpointGroupService - endpointRelation portainer.EndpointRelationService - registry portainer.RegistryService - resourceControl portainer.ResourceControlService - role portainer.RoleService - sslSettings portainer.SSLSettingsService - settings portainer.SettingsService - stack portainer.StackService - tag portainer.TagService - teamMembership portainer.TeamMembershipService - team portainer.TeamService - tunnelServer portainer.TunnelServerService - user portainer.UserService - version portainer.VersionService - webhook portainer.WebhookService + customTemplate portainer.CustomTemplateService + edgeGroup portainer.EdgeGroupService + edgeJob portainer.EdgeJobService + edgeStack portainer.EdgeStackService + endpoint portainer.EndpointService + endpointGroup portainer.EndpointGroupService + endpointRelation portainer.EndpointRelationService + helmUserRepository portainer.HelmUserRepositoryService + registry portainer.RegistryService + resourceControl portainer.ResourceControlService + role portainer.RoleService + sslSettings portainer.SSLSettingsService + settings portainer.SettingsService + stack portainer.StackService + tag portainer.TagService + teamMembership portainer.TeamMembershipService + team portainer.TeamService + tunnelServer portainer.TunnelServerService + user portainer.UserService + version portainer.VersionService + webhook portainer.WebhookService } func (d *datastore) BackupTo(io.Writer) error { return nil } @@ -45,19 +46,22 @@ func (d *datastore) EdgeStack() portainer.EdgeStackService { retur func (d *datastore) Endpoint() portainer.EndpointService { return d.endpoint } func (d *datastore) EndpointGroup() portainer.EndpointGroupService { return d.endpointGroup } func (d *datastore) EndpointRelation() portainer.EndpointRelationService { return d.endpointRelation } -func (d *datastore) Registry() portainer.RegistryService { return d.registry } -func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl } -func (d *datastore) Role() portainer.RoleService { return d.role } -func (d *datastore) Settings() portainer.SettingsService { return d.settings } -func (d *datastore) SSLSettings() portainer.SSLSettingsService { return d.sslSettings } -func (d *datastore) Stack() portainer.StackService { return d.stack } -func (d *datastore) Tag() portainer.TagService { return d.tag } -func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership } -func (d *datastore) Team() portainer.TeamService { return d.team } -func (d *datastore) TunnelServer() portainer.TunnelServerService { return d.tunnelServer } -func (d *datastore) User() portainer.UserService { return d.user } -func (d *datastore) Version() portainer.VersionService { return d.version } -func (d *datastore) Webhook() portainer.WebhookService { return d.webhook } +func (d *datastore) HelmUserRepository() portainer.HelmUserRepositoryService { + return d.helmUserRepository +} +func (d *datastore) Registry() portainer.RegistryService { return d.registry } +func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl } +func (d *datastore) Role() portainer.RoleService { return d.role } +func (d *datastore) Settings() portainer.SettingsService { return d.settings } +func (d *datastore) SSLSettings() portainer.SSLSettingsService { return d.sslSettings } +func (d *datastore) Stack() portainer.StackService { return d.stack } +func (d *datastore) Tag() portainer.TagService { return d.tag } +func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership } +func (d *datastore) Team() portainer.TeamService { return d.team } +func (d *datastore) TunnelServer() portainer.TunnelServerService { return d.tunnelServer } +func (d *datastore) User() portainer.UserService { return d.user } +func (d *datastore) Version() portainer.VersionService { return d.version } +func (d *datastore) Webhook() portainer.WebhookService { return d.webhook } type datastoreOption = func(d *datastore) @@ -71,21 +75,25 @@ func NewDatastore(options ...datastoreOption) *datastore { return &d } - type stubSettingsService struct { settings *portainer.Settings } -func (s *stubSettingsService) Settings() (*portainer.Settings, error) { return s.settings, nil } -func (s *stubSettingsService) UpdateSettings(settings *portainer.Settings) error { return nil } - -func WithSettings(settings *portainer.Settings) datastoreOption { +func (s *stubSettingsService) Settings() (*portainer.Settings, error) { + return s.settings, nil +} +func (s *stubSettingsService) UpdateSettings(settings *portainer.Settings) error { + s.settings = settings + return nil +} +func WithSettingsService(settings *portainer.Settings) datastoreOption { return func(d *datastore) { - d.settings = &stubSettingsService{settings: settings} + d.settings = &stubSettingsService{ + settings: settings, + } } } - type stubUserService struct { users []portainer.User } diff --git a/api/internal/testhelpers/integration.go b/api/internal/testhelpers/integration.go new file mode 100644 index 000000000..f7ad19be4 --- /dev/null +++ b/api/internal/testhelpers/integration.go @@ -0,0 +1,22 @@ +package testhelpers + +import ( + "flag" + "os" + "testing" +) + +var integration bool + +func init() { + flag.BoolVar(&integration, "integration", false, "enable integration tests") +} + +// IntegrationTest marks the current test as an integration test +func IntegrationTest(t *testing.T) { + _, enabled := os.LookupEnv("INTEGRATION_TEST") + + if !(integration || enabled) { + t.Skip("Skipping integration test") + } +} diff --git a/api/internal/testhelpers/request_bouncer.go b/api/internal/testhelpers/request_bouncer.go new file mode 100644 index 000000000..39d7bab39 --- /dev/null +++ b/api/internal/testhelpers/request_bouncer.go @@ -0,0 +1,15 @@ +package testhelpers + +import "net/http" + +type testRequestBouncer struct { +} + +// NewTestRequestBouncer creates new mock for requestBouncer +func NewTestRequestBouncer() *testRequestBouncer { + return &testRequestBouncer{} +} + +func (testRequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { + return h +} diff --git a/api/jwt/jwt_kubeconfig_test.go b/api/jwt/jwt_kubeconfig_test.go index b8269b4ca..32289d0f3 100644 --- a/api/jwt/jwt_kubeconfig_test.go +++ b/api/jwt/jwt_kubeconfig_test.go @@ -1,11 +1,12 @@ package jwt import ( + "testing" + "github.com/dgrijalva/jwt-go" portainer "github.com/portainer/portainer/api" i "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" - "testing" ) func TestService_GenerateTokenForKubeconfig(t *testing.T) { @@ -24,7 +25,7 @@ func TestService_GenerateTokenForKubeconfig(t *testing.T) { myFields := fields{ userSessionTimeout: "24h", - dataStore: i.NewDatastore(i.WithSettings(mySettings)), + dataStore: i.NewDatastore(i.WithSettingsService(mySettings)), } myTokenData := &portainer.TokenData{ @@ -78,4 +79,4 @@ func TestService_GenerateTokenForKubeconfig(t *testing.T) { assert.Equal(t, tt.wantExpiresAt, tokenClaims.ExpiresAt) }) } -} \ No newline at end of file +} diff --git a/api/kubernetes/kubeconfig_service.go b/api/kubernetes/kubeconfig_service.go new file mode 100644 index 000000000..49547b274 --- /dev/null +++ b/api/kubernetes/kubeconfig_service.go @@ -0,0 +1,104 @@ +package kubernetes + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + + "github.com/pkg/errors" + portainer "github.com/portainer/portainer/api" +) + +// KubeConfigService represents a service that is responsible for handling kubeconfig operations +type KubeConfigService interface { + IsSecure() bool + GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess +} + +// KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data +type kubernetesClusterAccess struct { + ClusterServerURL string `example:"https://mycompany.k8s.com"` + CertificateAuthorityFile string `example:"/data/tls/localhost.crt"` + CertificateAuthorityData string `example:"MIIC5TCCAc2gAwIBAgIJAJ+...+xuhOaFXwQ=="` + AuthToken string `example:"ey..."` +} + +type kubeConfigCAService struct { + httpsBindAddr string + certificateAuthorityFile string + certificateAuthorityData string +} + +var ( + errTLSCertNotProvided = errors.New("tls cert path not provided") + errTLSCertFileMissing = errors.New("missing tls cert file") + errTLSCertIncorrectType = errors.New("incorrect tls cert type") + errTLSCertValidation = errors.New("failed to parse tls certificate") +) + +// NewKubeConfigCAService encapsulates generation of core KubeConfig data +func NewKubeConfigCAService(httpsBindAddr string, tlsCertPath string) KubeConfigService { + certificateAuthorityData, err := getCertificateAuthorityData(tlsCertPath) + if err != nil { + log.Printf("[DEBUG] [internal,kubeconfig] [message: %s, generated KubeConfig will be insecure]", err.Error()) + } + + return &kubeConfigCAService{ + httpsBindAddr: httpsBindAddr, + certificateAuthorityFile: tlsCertPath, + certificateAuthorityData: certificateAuthorityData, + } +} + +// getCertificateAuthorityData reads tls certificate from supplied path and verifies the tls certificate +// then returns content (string) of the certificate within `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` +func getCertificateAuthorityData(tlsCertPath string) (string, error) { + if tlsCertPath == "" { + return "", errTLSCertNotProvided + } + + data, err := ioutil.ReadFile(tlsCertPath) + if err != nil { + return "", errors.Wrap(errTLSCertFileMissing, err.Error()) + } + + block, _ := pem.Decode(data) + if block == nil || block.Type != "CERTIFICATE" { + return "", errTLSCertIncorrectType + } + + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", errors.Wrap(errTLSCertValidation, err.Error()) + } + + return base64.StdEncoding.EncodeToString(certificate.Raw), nil +} + +// IsSecure specifies whether generated KubeConfig structs from the service will not have `insecure-skip-tls-verify: true` +// this is based on the fact that we can successfully extract `certificateAuthorityData` from +// certificate file at `tlsCertPath`. If we can successfully extract `certificateAuthorityData`, +// then this will be used as `certificate-authority-data` attribute in a generated KubeConfig. +func (kccas *kubeConfigCAService) IsSecure() bool { + return kccas.certificateAuthorityData != "" +} + +// GetKubeConfigInternal returns K8s cluster access details for the specified endpoint. +// On startup, portainer generates a certificate against localhost at specified `httpsBindAddr` port, hence +// the kubeconfig generated should only be utilised by internal portainer binaries as the `ClusterServerURL` +// points to the internally accessible `https` based `localhost` address. +// The struct can be used to: +// - generate a kubeconfig file +// - pass down params to binaries +func (kccas *kubeConfigCAService) GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess { + clusterServerUrl := fmt.Sprintf("https://localhost%s/api/endpoints/%s/kubernetes", kccas.httpsBindAddr, fmt.Sprint(endpointId)) + return kubernetesClusterAccess{ + ClusterServerURL: clusterServerUrl, + CertificateAuthorityFile: kccas.certificateAuthorityFile, + CertificateAuthorityData: kccas.certificateAuthorityData, + AuthToken: authToken, + } +} diff --git a/api/kubernetes/kubeconfig_service_test.go b/api/kubernetes/kubeconfig_service_test.go new file mode 100644 index 000000000..9b143f349 --- /dev/null +++ b/api/kubernetes/kubeconfig_service_test.go @@ -0,0 +1,149 @@ +package kubernetes + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TLS certificate can be generated using: +// openssl req -x509 -out localhost.crt -keyout localhost.key -newkey rsa:2048 -nodes -sha25 -subj '/CN=localhost' -extensions EXT -config <( \ +// printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +const certData = `-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIJAJ+poiEBdsplMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yMTA4MDQwNDM0MTZaFw0yMTA5MDMwNDM0MTZaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKQ0HStP34FY/lSDIfMG9MV/lKNUkiLZcMXepbyhPit4ND/w9kOA4WTJ+oP0 +B2IYklRvLkneZOfQiPweGAPwZl3CjwII6gL6NCkhcXXAJ4JQ9duL5Q6pL//95Ocv +X+qMTssyS1DcH88F6v+gifACLpvG86G9V0DeSGS2fqqfOJngrOCgum1DsWi3Xsew +B3A7GkPRjYmckU3t4iHgcMb+6lGQAxtnllSM9DpqGnjXRs4mnQHKgufaeW5nvHXi +oa5l0aHIhN6MQS99QwKwfml7UtWAYhSJksMrrTovB6rThYpp2ID/iU9MGfkpxubT +oA6scv8alFa8Bo+NEKo255dxsScCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo +b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEALFBHW/r79KOj5bhoDtHs8h/ESAlD5DJI/kzc1RajA8AuWPsaagG/ +S0Bqiq2ApMA6Tr3t9An8peaLCaUapWw59kyQcwwPXm9vxhEEfoBRtk8po8XblsUS +Q5Ku07ycSg5NBGEW2rCLsvjQFuQiAt8sW4jGCCN+ph/GQF9XC8ir+ssiqiMEkbm/ +JaK7sTi5kZ/GsSK8bJ+9N/ztoFr89YYEWjjOuIS3HNMdBcuQXIel7siEFdNjbzMo +iuViiuhTPJkxKOzCmv52cxf15B0/+cgcImoX4zc9Z0NxKthBmIe00ojexE0ZBOFi +4PxB7Ou6y/c9OvJb7gJv3z08+xuhOaFXwQ== +-----END CERTIFICATE----- +` + +// string within the `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` without linebreaks +const certDataString = "MIIC5TCCAc2gAwIBAgIJAJ+poiEBdsplMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMTA4MDQwNDM0MTZaFw0yMTA5MDMwNDM0MTZaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKQ0HStP34FY/lSDIfMG9MV/lKNUkiLZcMXepbyhPit4ND/w9kOA4WTJ+oP0B2IYklRvLkneZOfQiPweGAPwZl3CjwII6gL6NCkhcXXAJ4JQ9duL5Q6pL//95OcvX+qMTssyS1DcH88F6v+gifACLpvG86G9V0DeSGS2fqqfOJngrOCgum1DsWi3XsewB3A7GkPRjYmckU3t4iHgcMb+6lGQAxtnllSM9DpqGnjXRs4mnQHKgufaeW5nvHXioa5l0aHIhN6MQS99QwKwfml7UtWAYhSJksMrrTovB6rThYpp2ID/iU9MGfkpxubToA6scv8alFa8Bo+NEKo255dxsScCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEALFBHW/r79KOj5bhoDtHs8h/ESAlD5DJI/kzc1RajA8AuWPsaagG/S0Bqiq2ApMA6Tr3t9An8peaLCaUapWw59kyQcwwPXm9vxhEEfoBRtk8po8XblsUSQ5Ku07ycSg5NBGEW2rCLsvjQFuQiAt8sW4jGCCN+ph/GQF9XC8ir+ssiqiMEkbm/JaK7sTi5kZ/GsSK8bJ+9N/ztoFr89YYEWjjOuIS3HNMdBcuQXIel7siEFdNjbzMoiuViiuhTPJkxKOzCmv52cxf15B0/+cgcImoX4zc9Z0NxKthBmIe00ojexE0ZBOFi4PxB7Ou6y/c9OvJb7gJv3z08+xuhOaFXwQ==" + +func createTempFile(filename, content string) (string, func()) { + tempPath, _ := ioutil.TempDir("", "temp") + filePath := fmt.Sprintf("%s/%s", tempPath, filename) + ioutil.WriteFile(filePath, []byte(content), 0644) + + teardown := func() { os.RemoveAll(tempPath) } + + return filePath, teardown +} + +func Test_getCertificateAuthorityData(t *testing.T) { + is := assert.New(t) + + t.Run("getCertificateAuthorityData fails on tls cert not provided", func(t *testing.T) { + _, err := getCertificateAuthorityData("") + is.ErrorIs(err, errTLSCertNotProvided, "getCertificateAuthorityData should fail with %w", errTLSCertNotProvided) + }) + + t.Run("getCertificateAuthorityData fails on tls cert provided but missing file", func(t *testing.T) { + _, err := getCertificateAuthorityData("/tmp/non-existent.crt") + is.ErrorIs(err, errTLSCertFileMissing, "getCertificateAuthorityData should fail with %w", errTLSCertFileMissing) + }) + + t.Run("getCertificateAuthorityData fails on tls cert provided but invalid file data", func(t *testing.T) { + filePath, teardown := createTempFile("invalid-cert.crt", "hello\ngo\n") + defer teardown() + + _, err := getCertificateAuthorityData(filePath) + is.ErrorIs(err, errTLSCertIncorrectType, "getCertificateAuthorityData should fail with %w", errTLSCertIncorrectType) + }) + + t.Run("getCertificateAuthorityData succeeds on valid tls cert provided", func(t *testing.T) { + filePath, teardown := createTempFile("valid-cert.crt", certData) + defer teardown() + + certificateAuthorityData, err := getCertificateAuthorityData(filePath) + is.NoError(err, "getCertificateAuthorityData succeed with valid cert; err=%w", errTLSCertIncorrectType) + + is.Equal(certificateAuthorityData, certDataString, "returned certificateAuthorityData should be %s", certDataString) + }) +} + +func TestKubeConfigService_IsSecure(t *testing.T) { + is := assert.New(t) + + t.Run("IsSecure should be false", func(t *testing.T) { + kcs := NewKubeConfigCAService("", "") + is.False(kcs.IsSecure(), "should be false if TLS cert not provided") + }) + + t.Run("IsSecure should be false", func(t *testing.T) { + filePath, teardown := createTempFile("valid-cert.crt", certData) + defer teardown() + + kcs := NewKubeConfigCAService("", filePath) + is.True(kcs.IsSecure(), "should be true if valid TLS cert (path and content) provided") + }) +} + +func TestKubeConfigService_GetKubeConfigInternal(t *testing.T) { + is := assert.New(t) + + t.Run("GetKubeConfigInternal returns localhost address", func(t *testing.T) { + kcs := NewKubeConfigCAService("", "") + clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token") + is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://localhost"), "should contain localhost address") + }) + + t.Run("GetKubeConfigInternal contains https bind address port", func(t *testing.T) { + kcs := NewKubeConfigCAService(":1010", "") + clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token") + is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, ":1010"), "should contain bind address port") + }) + + t.Run("GetKubeConfigInternal contains endpoint proxy url", func(t *testing.T) { + kcs := NewKubeConfigCAService("", "") + clusterAccessDetails := kcs.GetKubeConfigInternal(100, "some-token") + is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "api/endpoints/100/kubernetes"), "should contain endpoint proxy url") + }) + + t.Run("GetKubeConfigInternal returns insecure cluster access config", func(t *testing.T) { + kcs := NewKubeConfigCAService("", "") + clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token") + + wantClusterAccessDetails := kubernetesClusterAccess{ + ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes", + AuthToken: "some-token", + CertificateAuthorityFile: "", + CertificateAuthorityData: "", + } + + is.Equal(clusterAccessDetails, wantClusterAccessDetails) + }) + + t.Run("GetKubeConfigInternal returns secure cluster access config", func(t *testing.T) { + filePath, teardown := createTempFile("valid-cert.crt", certData) + defer teardown() + + kcs := NewKubeConfigCAService("", filePath) + clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token") + + wantClusterAccessDetails := kubernetesClusterAccess{ + ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes", + AuthToken: "some-token", + CertificateAuthorityFile: filePath, + CertificateAuthorityData: certDataString, + } + + is.Equal(clusterAccessDetails, wantClusterAccessDetails) + }) +} diff --git a/api/kubernetes/validation/validation.go b/api/kubernetes/validation/validation.go new file mode 100644 index 000000000..26b26eac5 --- /dev/null +++ b/api/kubernetes/validation/validation.go @@ -0,0 +1,48 @@ +package validation + +// borrowed from apimachinery@v0.17.2/pkg/util/validation/validation.go +// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go + +import ( + "fmt" + "regexp" +) + +const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" +const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" +const DNS1123SubdomainMaxLength int = 253 + +var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") + +// IsDNS1123Subdomain tests for a string that conforms to the definition of a subdomain in DNS (RFC 1123). +func IsDNS1123Subdomain(value string) []string { + var errs []string + if len(value) > DNS1123SubdomainMaxLength { + errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength)) + } + if !dns1123SubdomainRegexp.MatchString(value) { + errs = append(errs, RegexError(dns1123SubdomainFmt, "example.com")) + } + return errs +} + +// MaxLenError returns a string explanation of a "string too long" validation failure. +func MaxLenError(length int) string { + return fmt.Sprintf("must be no more than %d characters", length) +} + +// RegexError returns a string explanation of a regex validation failure. +func RegexError(fmt string, examples ...string) string { + s := "must match the regex " + fmt + if len(examples) == 0 { + return s + } + s += " (e.g. " + for i := range examples { + if i > 0 { + s += " or " + } + s += "'" + examples[i] + "'" + } + return s + ")" +} diff --git a/api/portainer.go b/api/portainer.go index 703b3fd36..d70880f65 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -395,6 +395,18 @@ type ( ProjectPath string `json:"ProjectPath"` } + HelmUserRepositoryID int + + // HelmUserRepositories stores a Helm repository URL for the given user + HelmUserRepository struct { + // Membership Identifier + ID HelmUserRepositoryID `json:"Id" example:"1"` + // User identifier + UserID UserID `json:"UserId" example:"1"` + // Helm repository URL + URL string `json:"URL" example:"https://charts.bitnami.com/bitnami"` + } + // QuayRegistryData represents data required for Quay registry to work QuayRegistryData struct { UseOrganisation bool `json:"UseOrganisation"` @@ -699,6 +711,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"` // Deprecated fields DisplayDonationHeader bool @@ -1099,6 +1113,7 @@ type ( Endpoint() EndpointService EndpointGroup() EndpointGroupService EndpointRelation() EndpointRelationService + HelmUserRepository() HelmUserRepositoryService Registry() RegistryService ResourceControl() ResourceControlService Role() RoleService @@ -1226,6 +1241,12 @@ type ( LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) } + // HelmUserRepositoryService represents a service to manage HelmUserRepositories + HelmUserRepositoryService interface { + HelmUserRepositoryByUserID(userID UserID) ([]HelmUserRepository, error) + CreateHelmUserRepository(record *HelmUserRepository) error + } + // JWTService represents a service for managing JWT tokens JWTService interface { GenerateToken(data *TokenData) (string, error) @@ -1466,6 +1487,8 @@ const ( DefaultEdgeAgentCheckinIntervalInSeconds = 5 // DefaultTemplatesURL represents the URL to the official templates supported by Portainer DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json" + // DefaultHelmrepositoryURL represents the URL to the official templates supported by Bitnami + DefaultHelmRepositoryURL = "https://charts.bitnami.com/bitnami" // 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/assets/css/app.css b/app/assets/css/app.css index 74b1788b3..c75fcd32d 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -310,6 +310,10 @@ a[ng-click] { padding-top: 15px !important; } +.nomargin { + margin: 0 !important; +} + .terminal-container { width: 100%; padding: 10px 0; @@ -833,3 +837,8 @@ json-tree .branch-preview { text-align: center; padding-bottom: 5px; } + +.text-wrap { + word-break: break-all; + white-space: normal; +} diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 7bb3d3739..853c7a641 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -45,6 +45,26 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo }, }; + const helmApplication = { + name: 'kubernetes.helm', + url: '/helm/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesHelmApplicationView', + }, + }, + }; + + const helmTemplates = { + name: 'kubernetes.templates.helm', + url: '/helm', + views: { + 'content@': { + component: 'helmTemplatesView', + }, + }, + }; + const applications = { name: 'kubernetes.applications', url: '/applications', @@ -301,6 +321,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo }; $stateRegistryProvider.register(kubernetes); + $stateRegistryProvider.register(helmApplication); + $stateRegistryProvider.register(helmTemplates); $stateRegistryProvider.register(applications); $stateRegistryProvider.register(applicationCreation); $stateRegistryProvider.register(application); diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js new file mode 100644 index 000000000..b20b42ec9 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.controller.js @@ -0,0 +1,12 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +export default class { + $onInit() { + const secrets = (this.configurations || []) + .filter((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET) + .flatMap((config) => Object.entries(config.Data)) + .map(([key, value]) => ({ key, value })); + + this.state = { secrets }; + } +} diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html new file mode 100644 index 000000000..7d9cc659e --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.html @@ -0,0 +1,12 @@ +
+ Secrets +
+
+ + + + + +
+ +
diff --git a/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js new file mode 100644 index 000000000..410563560 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-details/applications-datatable-details.js @@ -0,0 +1,10 @@ +import angular from 'angular'; +import controller from './applications-datatable-details.controller'; + +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableDetails', { + templateUrl: './applications-datatable-details.html', + controller, + bindings: { + configurations: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css new file mode 100644 index 000000000..17c283bf7 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.css @@ -0,0 +1,10 @@ +.published-url-container { + display: grid; + grid-template-columns: 1fr 1fr 3fr; + padding-top: 1rem; + padding-bottom: 0.5rem; +} + +.publish-url-link { + width: min-content; +} diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html new file mode 100644 index 000000000..ea25b014c --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.html @@ -0,0 +1,6 @@ +
+
+ Published URL +
+ {{ $ctrl.publishedUrl }} +
diff --git a/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js new file mode 100644 index 000000000..7177b851b --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable-url/applications-datatable-url.js @@ -0,0 +1,9 @@ +import angular from 'angular'; +import './applications-datatable-url.css'; + +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableUrl', { + templateUrl: './applications-datatable-url.html', + bindings: { + publishedUrl: '@', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css new file mode 100644 index 000000000..6cf9da69a --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.css @@ -0,0 +1,16 @@ +.secondary-heading { + background-color: #e7f6ff !important; +} + +.secondary-body { + background-color: #f1f9fd; +} + +.datatable-wide { + width: 55px; +} + +.datatable-padding-vertical { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index 3a2517be6..1f1d46fdd 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -1,7 +1,7 @@
-
+
{{ $ctrl.titleText }}
@@ -62,7 +62,7 @@
-
+
-