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: ( + <> +