From 85a7b7e0fcf96dc480cb8ce1b89754c904419eb2 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 14 Apr 2022 13:14:23 +0300 Subject: [PATCH] refactor(edge): move edge deploy script to react [EE-2689] (#6747) --- api/http/handler/endpoints/endpoint_create.go | 16 +- api/http/handler/settings/settings_update.go | 14 + api/internal/edge/url.go | 28 ++ api/portainer.go | 2 + app/edge/__module.js | 3 +- .../EdgeScriptForm/EdgePropertiesForm.tsx | 75 +++++ .../EdgeScriptForm/EdgeScriptForm.tsx | 60 ++++ .../components/EdgeScriptForm/OsSelector.tsx | 45 +++ .../components/EdgeScriptForm/Scripts.tsx | 293 ++++++++++++++++++ app/edge/components/EdgeScriptForm/index.ts | 1 + app/edge/components/EdgeScriptForm/types.ts | 10 + app/edge/components/index.ts | 7 + .../Button/CopyButton/CopyButton.tsx | 45 +-- .../components/Button/CopyButton/useCopy.ts | 37 +++ app/portainer/components/Code/Code.module.css | 13 + .../components/Code/Code.stories.tsx | 30 +- app/portainer/components/Code/Code.tsx | 32 +- .../components/NavTabs/NavTabs.module.css | 32 ++ .../components/NavTabs/NavTabs.stories.tsx | 31 ++ .../components/NavTabs/NavTabs.test.tsx | 58 ++++ app/portainer/components/NavTabs/NavTabs.tsx | 60 ++++ .../ButtonSelector/ButtonSelector.tsx | 4 +- .../environments/environment.service/edge.ts | 16 + app/portainer/models/settings.js | 1 + app/portainer/services/api/status.service.ts | 23 ++ .../AutomaticEdgeEnvCreation.tsx | 39 +++ .../AutomaticEdgeEnvCreation/index.ts | 1 + .../EdgeComputeSettings.module.css} | 0 .../EdgeComputeSettings.tsx} | 49 ++- .../EdgeComputeSettings.validation.ts} | 10 +- .../edge-compute/EdgeComputeSettings/index.ts | 1 + .../edge-compute/EdgeComputeSettingsView.tsx | 25 ++ .../edge-compute/SettingsEdgeCompute/index.ts | 9 - app/portainer/settings/edge-compute/types.ts | 7 + app/portainer/settings/index.js | 4 +- app/portainer/settings/settings.service.ts | 47 +++ .../views/endpoints/edit/endpoint.html | 113 +++---- .../endpoints/edit/endpointController.js | 180 +---------- 38 files changed, 1079 insertions(+), 342 deletions(-) create mode 100644 api/internal/edge/url.go create mode 100644 app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx create mode 100644 app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx create mode 100644 app/edge/components/EdgeScriptForm/OsSelector.tsx create mode 100644 app/edge/components/EdgeScriptForm/Scripts.tsx create mode 100644 app/edge/components/EdgeScriptForm/index.ts create mode 100644 app/edge/components/EdgeScriptForm/types.ts create mode 100644 app/edge/components/index.ts create mode 100644 app/portainer/components/Button/CopyButton/useCopy.ts create mode 100644 app/portainer/components/NavTabs/NavTabs.module.css create mode 100644 app/portainer/components/NavTabs/NavTabs.stories.tsx create mode 100644 app/portainer/components/NavTabs/NavTabs.test.tsx create mode 100644 app/portainer/components/NavTabs/NavTabs.tsx create mode 100644 app/portainer/environments/environment.service/edge.ts create mode 100644 app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx create mode 100644 app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/index.ts rename app/portainer/settings/edge-compute/{SettingsEdgeCompute/SettingsEdgeCompute.module.css => EdgeComputeSettings/EdgeComputeSettings.module.css} (100%) rename app/portainer/settings/edge-compute/{SettingsEdgeCompute/SettingsEdgeCompute.tsx => EdgeComputeSettings/EdgeComputeSettings.tsx} (76%) rename app/portainer/settings/edge-compute/{SettingsEdgeCompute/SettingsEdgeCompute.validation.ts => EdgeComputeSettings/EdgeComputeSettings.validation.ts} (55%) create mode 100644 app/portainer/settings/edge-compute/EdgeComputeSettings/index.ts create mode 100644 app/portainer/settings/edge-compute/EdgeComputeSettingsView.tsx delete mode 100644 app/portainer/settings/edge-compute/SettingsEdgeCompute/index.ts create mode 100644 app/portainer/settings/edge-compute/types.ts diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 7a2a08827..9a87a2804 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -3,7 +3,6 @@ package endpoints import ( "errors" "fmt" - "net" "net/http" "net/url" "runtime" @@ -293,7 +292,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 environment", err} + return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occurred while trying to create the environment", err} } return endpoint, nil @@ -302,18 +301,9 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() - portainerURL, err := url.Parse(payload.URL) + portainerHost, err := edge.ParseHostForEdge(payload.URL) if err != nil { - return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", err} - } - - portainerHost, _, err := net.SplitHostPort(portainerURL.Host) - if err != nil { - portainerHost = portainerURL.Host - } - - if portainerHost == "localhost" { - return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", errors.New("cannot use localhost as environment URL")} + return nil, httperror.BadRequest("Unable to parse host", err) } edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID) diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 6a5d5553a..895cf0128 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -13,6 +13,7 @@ import ( "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/internal/edge" ) type settingsUpdatePayload struct { @@ -46,6 +47,8 @@ type settingsUpdatePayload struct { DisableTrustOnFirstConnect *bool `example:"false"` // EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone EnforceEdgeID *bool `example:"false"` + // EdgePortainerURL is the URL that is exposed to edge agents + EdgePortainerURL *string `json:"EdgePortainerURL"` } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -74,6 +77,13 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error { } } + if payload.EdgePortainerURL != nil { + _, err := edge.ParseHostForEdge(*payload.EdgePortainerURL) + if err != nil { + return err + } + } + return nil } @@ -178,6 +188,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.EnforceEdgeID = *payload.EnforceEdgeID } + if payload.EdgePortainerURL != nil { + settings.EdgePortainerURL = *payload.EdgePortainerURL + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/internal/edge/url.go b/api/internal/edge/url.go new file mode 100644 index 000000000..984eb421f --- /dev/null +++ b/api/internal/edge/url.go @@ -0,0 +1,28 @@ +package edge + +import ( + "net" + "net/url" + + "github.com/pkg/errors" +) + +// ParseHostForEdge returns the hostname of the given URL, will fail if host is localhost +func ParseHostForEdge(portainerURL string) (string, error) { + parsedURL, err := url.Parse(portainerURL) + if err != nil { + return "", errors.Wrap(err, "Unable to parse Portainer URL") + } + + portainerHost, _, err := net.SplitHostPort(parsedURL.Host) + if err != nil { + portainerHost = parsedURL.Host + } + + if portainerHost == "localhost" { + return "", errors.New("cannot use localhost as environment URL") + } + + return portainerHost, nil + +} diff --git a/api/portainer.go b/api/portainer.go index ff7f88981..2f942ea82 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -815,6 +815,8 @@ type ( EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"` // Container environment parameter AGENT_SECRET AgentSecret string `json:"AgentSecret"` + // EdgePortainerURL is the URL that is exposed to edge agents + EdgePortainerURL string `json:"EdgePortainerUrl"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/edge/__module.js b/app/edge/__module.js index 94affb39f..2f1561757 100644 --- a/app/edge/__module.js +++ b/app/edge/__module.js @@ -2,8 +2,9 @@ import angular from 'angular'; import edgeStackModule from './views/edge-stacks'; import edgeDevicesModule from './devices'; +import { componentsModule } from './components'; -angular.module('portainer.edge', [edgeStackModule, edgeDevicesModule]).config(function config($stateRegistryProvider) { +angular.module('portainer.edge', [edgeStackModule, edgeDevicesModule, componentsModule]).config(function config($stateRegistryProvider) { const edge = { name: 'edge', url: '/edge', diff --git a/app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx b/app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx new file mode 100644 index 000000000..622631ea1 --- /dev/null +++ b/app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx @@ -0,0 +1,75 @@ +import { FormControl } from '@/portainer/components/form-components/FormControl'; +import { Input } from '@/portainer/components/form-components/Input'; +import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle'; +import { SwitchField } from '@/portainer/components/form-components/SwitchField'; + +import { OsSelector } from './OsSelector'; +import { EdgeProperties } from './types'; + +interface Props { + setFieldValue(key: string, value: T): void; + values: EdgeProperties; + hideIdGetter: boolean; +} + +export function EdgePropertiesForm({ + setFieldValue, + values, + hideIdGetter, +}: Props) { + return ( +
+ Edge script settings + + setFieldValue('os', os)} + /> + + {!hideIdGetter && ( + + setFieldValue(e.target.name, e.target.value)} + /> + + )} + +
+
+ + setFieldValue('allowSelfSignedCertificates', checked) + } + /> +
+
+ + {values.platform !== 'k8s' && ( + + setFieldValue(e.target.name, e.target.value)} + /> + + )} + + ); +} diff --git a/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx b/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx new file mode 100644 index 000000000..737f235e7 --- /dev/null +++ b/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; + +import { useStatus } from '@/portainer/services/api/status.service'; +import { r2a } from '@/react-tools/react2angular'; +import { useSettings } from '@/portainer/settings/settings.service'; + +import { EdgePropertiesForm } from './EdgePropertiesForm'; +import { Scripts } from './Scripts'; +import { EdgeProperties } from './types'; + +interface Props { + edgeKey: string; + edgeId?: string; +} + +export function EdgeScriptForm({ edgeKey, edgeId }: Props) { + const [edgeProperties, setEdgeProperties] = useState({ + allowSelfSignedCertificates: true, + envVars: '', + edgeIdGenerator: '', + os: 'linux', + platform: 'swarm', + }); + + const settingsQuery = useSettings((settings) => settings.AgentSecret); + + const versionQuery = useStatus((status) => status.Version); + + if (!versionQuery.data) { + return null; + } + + const agentVersion = versionQuery.data; + const agentSecret = settingsQuery.data; + + return ( + <> + + setEdgeProperties({ ...edgeProperties, [key]: value }) + } + values={edgeProperties} + hideIdGetter={edgeId !== undefined} + /> + + + setEdgeProperties({ ...edgeProperties, platform }) + } + edgeId={edgeId} + agentSecret={agentSecret} + /> + + ); +} + +export const EdgeScriptFormAngular = r2a(EdgeScriptForm, ['edgeKey', 'edgeId']); diff --git a/app/edge/components/EdgeScriptForm/OsSelector.tsx b/app/edge/components/EdgeScriptForm/OsSelector.tsx new file mode 100644 index 000000000..8682414b5 --- /dev/null +++ b/app/edge/components/EdgeScriptForm/OsSelector.tsx @@ -0,0 +1,45 @@ +import { ButtonSelector } from '@/portainer/components/form-components/ButtonSelector/ButtonSelector'; + +import { OS } from './types'; + +interface Props { + value: OS; + onChange(value: OS): void; +} + +export function OsSelector({ onChange, value }: Props) { + return ( +
+
+ onChange(os)} + options={[ + { + value: 'linux', + label: ( + <> +