mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
refactor(edge): move edge deploy script to react [EE-2689] (#6747)
This commit is contained in:
parent
328ce2f995
commit
85a7b7e0fc
38 changed files with 1079 additions and 342 deletions
|
@ -3,7 +3,6 @@ package endpoints
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -293,7 +292,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
|
|
||||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||||
if err != nil {
|
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
|
return endpoint, nil
|
||||||
|
@ -302,18 +301,9 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||||
|
|
||||||
portainerURL, err := url.Parse(payload.URL)
|
portainerHost, err := edge.ParseHostForEdge(payload.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", err}
|
return nil, httperror.BadRequest("Unable to parse host", 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")}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge"
|
||||||
)
|
)
|
||||||
|
|
||||||
type settingsUpdatePayload struct {
|
type settingsUpdatePayload struct {
|
||||||
|
@ -46,6 +47,8 @@ type settingsUpdatePayload struct {
|
||||||
DisableTrustOnFirstConnect *bool `example:"false"`
|
DisableTrustOnFirstConnect *bool `example:"false"`
|
||||||
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
|
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
|
||||||
EnforceEdgeID *bool `example:"false"`
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +188,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
settings.EnforceEdgeID = *payload.EnforceEdgeID
|
settings.EnforceEdgeID = *payload.EnforceEdgeID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.EdgePortainerURL != nil {
|
||||||
|
settings.EdgePortainerURL = *payload.EdgePortainerURL
|
||||||
|
}
|
||||||
|
|
||||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||||
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
28
api/internal/edge/url.go
Normal file
28
api/internal/edge/url.go
Normal file
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -815,6 +815,8 @@ type (
|
||||||
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
|
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
|
||||||
// Container environment parameter AGENT_SECRET
|
// Container environment parameter AGENT_SECRET
|
||||||
AgentSecret string `json:"AgentSecret"`
|
AgentSecret string `json:"AgentSecret"`
|
||||||
|
// EdgePortainerURL is the URL that is exposed to edge agents
|
||||||
|
EdgePortainerURL string `json:"EdgePortainerUrl"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
|
|
|
@ -2,8 +2,9 @@ import angular from 'angular';
|
||||||
|
|
||||||
import edgeStackModule from './views/edge-stacks';
|
import edgeStackModule from './views/edge-stacks';
|
||||||
import edgeDevicesModule from './devices';
|
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 = {
|
const edge = {
|
||||||
name: 'edge',
|
name: 'edge',
|
||||||
url: '/edge',
|
url: '/edge',
|
||||||
|
|
75
app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx
Normal file
75
app/edge/components/EdgeScriptForm/EdgePropertiesForm.tsx
Normal file
|
@ -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<T>(key: string, value: T): void;
|
||||||
|
values: EdgeProperties;
|
||||||
|
hideIdGetter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EdgePropertiesForm({
|
||||||
|
setFieldValue,
|
||||||
|
values,
|
||||||
|
hideIdGetter,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<form className="form-horizontal">
|
||||||
|
<FormSectionTitle>Edge script settings</FormSectionTitle>
|
||||||
|
|
||||||
|
<OsSelector
|
||||||
|
value={values.os}
|
||||||
|
onChange={(os) => setFieldValue('os', os)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!hideIdGetter && (
|
||||||
|
<FormControl
|
||||||
|
label="Edge ID Generator"
|
||||||
|
tooltip="A bash script one liner that will generate the edge id"
|
||||||
|
inputId="edge-id-generator-input"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="edgeIdGenerator"
|
||||||
|
value={values.edgeIdGenerator}
|
||||||
|
id="edge-id-generator-input"
|
||||||
|
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<SwitchField
|
||||||
|
checked={values.allowSelfSignedCertificates}
|
||||||
|
label="Allow self-signed certificates"
|
||||||
|
tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS"
|
||||||
|
onChange={(checked) =>
|
||||||
|
setFieldValue('allowSelfSignedCertificates', checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{values.platform !== 'k8s' && (
|
||||||
|
<FormControl
|
||||||
|
label="Environment variables"
|
||||||
|
tooltip="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
|
||||||
|
inputId="env-vars-input"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="envVars"
|
||||||
|
value={values.envVars}
|
||||||
|
id="env-vars-input"
|
||||||
|
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
60
app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
Normal file
60
app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx
Normal file
|
@ -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<EdgeProperties>({
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<EdgePropertiesForm
|
||||||
|
setFieldValue={(key, value) =>
|
||||||
|
setEdgeProperties({ ...edgeProperties, [key]: value })
|
||||||
|
}
|
||||||
|
values={edgeProperties}
|
||||||
|
hideIdGetter={edgeId !== undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Scripts
|
||||||
|
values={edgeProperties}
|
||||||
|
agentVersion={agentVersion}
|
||||||
|
edgeKey={edgeKey}
|
||||||
|
onPlatformChange={(platform) =>
|
||||||
|
setEdgeProperties({ ...edgeProperties, platform })
|
||||||
|
}
|
||||||
|
edgeId={edgeId}
|
||||||
|
agentSecret={agentSecret}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EdgeScriptFormAngular = r2a(EdgeScriptForm, ['edgeKey', 'edgeId']);
|
45
app/edge/components/EdgeScriptForm/OsSelector.tsx
Normal file
45
app/edge/components/EdgeScriptForm/OsSelector.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<ButtonSelector
|
||||||
|
size="small"
|
||||||
|
value={value}
|
||||||
|
onChange={(os: OS) => onChange(os)}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'linux',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
<i className="fab fa-linux space-right" aria-hidden="true" />
|
||||||
|
Linux
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'win',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
<i
|
||||||
|
className="fab fa-windows space-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Windows
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
293
app/edge/components/EdgeScriptForm/Scripts.tsx
Normal file
293
app/edge/components/EdgeScriptForm/Scripts.tsx
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { Code } from '@/portainer/components/Code';
|
||||||
|
import { CopyButton } from '@/portainer/components/Button/CopyButton';
|
||||||
|
import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
|
||||||
|
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
|
||||||
|
|
||||||
|
import { EdgeProperties, Platform } from './types';
|
||||||
|
|
||||||
|
const commandsByOs = {
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
id: 'swarm',
|
||||||
|
label: 'Docker Swarm',
|
||||||
|
command: buildLinuxSwarmCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standalone',
|
||||||
|
label: 'Docker Standalone',
|
||||||
|
command: buildLinuxStandaloneCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k8s',
|
||||||
|
label: 'Kubernetes',
|
||||||
|
command: buildKubernetesCommand,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
win: [
|
||||||
|
{
|
||||||
|
id: 'swarm',
|
||||||
|
label: 'Docker Swarm',
|
||||||
|
command: buildWindowsSwarmCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standalone',
|
||||||
|
label: 'Docker Standalone',
|
||||||
|
command: buildWindowsStandaloneCommand,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
values: EdgeProperties;
|
||||||
|
edgeKey: string;
|
||||||
|
agentVersion: string;
|
||||||
|
edgeId?: string;
|
||||||
|
agentSecret?: string;
|
||||||
|
onPlatformChange(platform: Platform): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Scripts({
|
||||||
|
agentVersion,
|
||||||
|
values,
|
||||||
|
edgeKey,
|
||||||
|
edgeId,
|
||||||
|
agentSecret,
|
||||||
|
onPlatformChange,
|
||||||
|
}: Props) {
|
||||||
|
const {
|
||||||
|
os,
|
||||||
|
allowSelfSignedCertificates,
|
||||||
|
edgeIdGenerator,
|
||||||
|
envVars,
|
||||||
|
platform,
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!commandsByOs[os].find((p) => p.id === platform)) {
|
||||||
|
onPlatformChange('standalone');
|
||||||
|
}
|
||||||
|
}, [os, platform, onPlatformChange]);
|
||||||
|
|
||||||
|
const options = commandsByOs[os].map((c) => {
|
||||||
|
const cmd = c.command(
|
||||||
|
agentVersion,
|
||||||
|
edgeIdGenerator,
|
||||||
|
edgeKey,
|
||||||
|
allowSelfSignedCertificates,
|
||||||
|
envVars,
|
||||||
|
edgeId,
|
||||||
|
agentSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
label: c.label,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<Code>{cmd}</Code>
|
||||||
|
<CopyButton copyText={cmd}>Copy</CopyButton>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<NavTabs
|
||||||
|
selectedId={platform}
|
||||||
|
options={options}
|
||||||
|
onSelect={(id: Platform) => onPlatformChange(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
|
||||||
|
const vars = defaultVars.concat(
|
||||||
|
envVars.split(',').filter((s) => s.length > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
return vars.map((s) => `-e ${s}`).join(' \\\n ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLinuxStandaloneCommand(
|
||||||
|
agentVersion: string,
|
||||||
|
edgeIdScript: string,
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
envVars: string,
|
||||||
|
edgeId?: string,
|
||||||
|
agentSecret?: string
|
||||||
|
) {
|
||||||
|
const env = buildDockerEnvVars(
|
||||||
|
envVars,
|
||||||
|
buildDefaultEnvVars(
|
||||||
|
edgeKey,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
!edgeIdScript ? edgeId : undefined,
|
||||||
|
agentSecret
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}
|
||||||
|
docker run -d \\
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||||
|
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||||
|
-v /:/host \\
|
||||||
|
-v portainer_agent_data:/data \\
|
||||||
|
--restart always \\
|
||||||
|
${env} \\
|
||||||
|
--name portainer_edge_agent \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWindowsStandaloneCommand(
|
||||||
|
agentVersion: string,
|
||||||
|
edgeIdScript: string,
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
envVars: string,
|
||||||
|
edgeId?: string,
|
||||||
|
agentSecret?: string
|
||||||
|
) {
|
||||||
|
const env = buildDockerEnvVars(
|
||||||
|
envVars,
|
||||||
|
buildDefaultEnvVars(
|
||||||
|
edgeKey,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||||
|
agentSecret
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return `${
|
||||||
|
edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : ''
|
||||||
|
}
|
||||||
|
docker run -d \\
|
||||||
|
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||||
|
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||||
|
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||||
|
--restart always \\
|
||||||
|
${env} \\
|
||||||
|
--name portainer_edge_agent \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLinuxSwarmCommand(
|
||||||
|
agentVersion: string,
|
||||||
|
edgeIdScript: string,
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
envVars: string,
|
||||||
|
edgeId?: string,
|
||||||
|
agentSecret?: string
|
||||||
|
) {
|
||||||
|
const env = buildDockerEnvVars(envVars, [
|
||||||
|
...buildDefaultEnvVars(
|
||||||
|
edgeKey,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
!edgeIdScript ? edgeId : undefined,
|
||||||
|
agentSecret
|
||||||
|
),
|
||||||
|
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}
|
||||||
|
docker network create \\
|
||||||
|
--driver overlay \\
|
||||||
|
portainer_agent_network;
|
||||||
|
|
||||||
|
docker service create \\
|
||||||
|
--name portainer_edge_agent \\
|
||||||
|
--network portainer_agent_network \\
|
||||||
|
${env} \\
|
||||||
|
--mode global \\
|
||||||
|
--constraint 'node.platform.os == linux' \\
|
||||||
|
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
|
||||||
|
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\
|
||||||
|
--mount type=bind,src=//,dst=/host \\
|
||||||
|
--mount type=volume,src=portainer_agent_data,dst=/data \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWindowsSwarmCommand(
|
||||||
|
agentVersion: string,
|
||||||
|
edgeIdScript: string,
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
envVars: string,
|
||||||
|
edgeId?: string,
|
||||||
|
agentSecret?: string
|
||||||
|
) {
|
||||||
|
const env = buildDockerEnvVars(envVars, [
|
||||||
|
...buildDefaultEnvVars(
|
||||||
|
edgeKey,
|
||||||
|
allowSelfSignedCerts,
|
||||||
|
edgeIdScript ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||||
|
agentSecret
|
||||||
|
),
|
||||||
|
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `${
|
||||||
|
edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : ''
|
||||||
|
}
|
||||||
|
docker network create \\
|
||||||
|
--driver overlay \\
|
||||||
|
portainer_agent_network;
|
||||||
|
|
||||||
|
docker service create \\
|
||||||
|
--name portainer_edge_agent \\
|
||||||
|
--network portainer_agent_network \\
|
||||||
|
${env} \\
|
||||||
|
--mode global \\
|
||||||
|
--constraint 'node.platform.os == windows' \\
|
||||||
|
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||||
|
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||||
|
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKubernetesCommand(
|
||||||
|
agentVersion: string,
|
||||||
|
edgeIdScript: string,
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
_envVars: string,
|
||||||
|
edgeId?: string,
|
||||||
|
agentSecret = ''
|
||||||
|
) {
|
||||||
|
const agentShortVersion = getAgentShortVersion(agentVersion);
|
||||||
|
const idEnvVar = edgeIdScript
|
||||||
|
? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `${idEnvVar}curl https://downloads.portainer.io/portainer-ee${agentShortVersion}-edge-agent-setup.sh |
|
||||||
|
bash -s -- "${
|
||||||
|
!edgeIdScript && edgeId ? edgeId : '$PORTAINER_EDGE_ID'
|
||||||
|
}" "${edgeKey}" "${allowSelfSignedCerts ? '1' : '0'}" "${agentSecret}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultEnvVars(
|
||||||
|
edgeKey: string,
|
||||||
|
allowSelfSignedCerts: boolean,
|
||||||
|
edgeId = '$PORTAINER_EDGE_ID',
|
||||||
|
agentSecret = ''
|
||||||
|
) {
|
||||||
|
return _.compact([
|
||||||
|
'EDGE=1',
|
||||||
|
`EDGE_ID=${edgeId}`,
|
||||||
|
`EDGE_KEY=${edgeKey}`,
|
||||||
|
`EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0}`,
|
||||||
|
agentSecret ? `AGENT_SECRET=${agentSecret}` : ``,
|
||||||
|
]);
|
||||||
|
}
|
1
app/edge/components/EdgeScriptForm/index.ts
Normal file
1
app/edge/components/EdgeScriptForm/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { EdgeScriptForm, EdgeScriptFormAngular } from './EdgeScriptForm';
|
10
app/edge/components/EdgeScriptForm/types.ts
Normal file
10
app/edge/components/EdgeScriptForm/types.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export type Platform = 'standalone' | 'swarm' | 'k8s';
|
||||||
|
export type OS = 'win' | 'linux';
|
||||||
|
|
||||||
|
export interface EdgeProperties {
|
||||||
|
os: OS;
|
||||||
|
allowSelfSignedCertificates: boolean;
|
||||||
|
envVars: string;
|
||||||
|
edgeIdGenerator: string;
|
||||||
|
platform: Platform;
|
||||||
|
}
|
7
app/edge/components/index.ts
Normal file
7
app/edge/components/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { EdgeScriptFormAngular } from './EdgeScriptForm';
|
||||||
|
|
||||||
|
export const componentsModule = angular
|
||||||
|
.module('app.edge.components', [])
|
||||||
|
.component('edgeScriptForm', EdgeScriptFormAngular).name;
|
|
@ -1,9 +1,10 @@
|
||||||
import { PropsWithChildren, useState, useEffect } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
|
|
||||||
import styles from './CopyButton.module.css';
|
import styles from './CopyButton.module.css';
|
||||||
|
import { useCopy } from './useCopy';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
copyText: string;
|
copyText: string;
|
||||||
|
@ -19,49 +20,29 @@ export function CopyButton({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const [isFading, setIsFading] = useState(false);
|
const { handleCopy, copiedSuccessfully } = useCopy(copyText, fadeDelay);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fadeoutTime = setTimeout(() => setIsFading(false), fadeDelay);
|
|
||||||
// clear timeout when component unmounts
|
|
||||||
return () => {
|
|
||||||
clearTimeout(fadeoutTime);
|
|
||||||
};
|
|
||||||
}, [isFading, fadeDelay]);
|
|
||||||
|
|
||||||
function onClick() {
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
|
|
||||||
// https://caniuse.com/?search=clipboard
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(copyText);
|
|
||||||
} else {
|
|
||||||
// https://stackoverflow.com/a/57192718
|
|
||||||
const inputEl = document.createElement('input');
|
|
||||||
inputEl.value = copyText;
|
|
||||||
inputEl.type = 'text';
|
|
||||||
document.body.appendChild(inputEl);
|
|
||||||
inputEl.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
inputEl.type = 'hidden';
|
|
||||||
document.body.removeChild(inputEl);
|
|
||||||
}
|
|
||||||
setIsFading(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Button
|
<Button
|
||||||
className={className}
|
className={className}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={onClick}
|
onClick={handleCopy}
|
||||||
title="Copy Value"
|
title="Copy Value"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="fa fa-copy space-right" aria-hidden="true" /> {children}
|
<i className="fa fa-copy space-right" aria-hidden="true" /> {children}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<span className={clsx(isFading && styles.fadeout, styles.displayText)}>
|
<span
|
||||||
<i className="fa fa-check" aria-hidden="true" /> {displayText}
|
className={clsx(
|
||||||
|
copiedSuccessfully && styles.fadeout,
|
||||||
|
styles.displayText,
|
||||||
|
'space-left'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<i className="fa fa-check" aria-hidden="true" />
|
||||||
|
{displayText && <span className="space-left">{displayText}</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
37
app/portainer/components/Button/CopyButton/useCopy.ts
Normal file
37
app/portainer/components/Button/CopyButton/useCopy.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useCopy(copyText: string, fadeDelay = 1000) {
|
||||||
|
const [copiedSuccessfully, setCopiedSuccessfully] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fadeoutTime = setTimeout(
|
||||||
|
() => setCopiedSuccessfully(false),
|
||||||
|
fadeDelay
|
||||||
|
);
|
||||||
|
// clear timeout when component unmounts
|
||||||
|
return () => {
|
||||||
|
clearTimeout(fadeoutTime);
|
||||||
|
};
|
||||||
|
}, [copiedSuccessfully, fadeDelay]);
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
|
||||||
|
// https://caniuse.com/?search=clipboard
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(copyText);
|
||||||
|
} else {
|
||||||
|
// https://stackoverflow.com/a/57192718
|
||||||
|
const inputEl = document.createElement('input');
|
||||||
|
inputEl.value = copyText;
|
||||||
|
inputEl.type = 'text';
|
||||||
|
document.body.appendChild(inputEl);
|
||||||
|
inputEl.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
inputEl.type = 'hidden';
|
||||||
|
document.body.removeChild(inputEl);
|
||||||
|
}
|
||||||
|
setCopiedSuccessfully(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { handleCopy, copiedSuccessfully };
|
||||||
|
}
|
|
@ -3,3 +3,16 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
padding: 16px 90px;
|
padding: 16px 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: min(20px, 25%);
|
||||||
|
right: 20px;
|
||||||
|
padding: 0;
|
||||||
|
outline: none !important;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Meta, Story } from '@storybook/react';
|
import { Meta, Story } from '@storybook/react';
|
||||||
import { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
import { Code } from './Code';
|
import { Code } from './Code';
|
||||||
|
|
||||||
|
@ -8,19 +7,28 @@ export default {
|
||||||
title: 'Components/Code',
|
title: 'Components/Code',
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
function Template({
|
interface Args {
|
||||||
children,
|
text: string;
|
||||||
}: JSX.IntrinsicAttributes & PropsWithChildren<unknown>) {
|
showCopyButton?: boolean;
|
||||||
return <Code>{children}</Code>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Primary: Story<PropsWithChildren<unknown>> = Template.bind({});
|
function Template({ text, showCopyButton }: Args) {
|
||||||
|
return <Code showCopyButton={showCopyButton}>{text}</Code>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<Args> = Template.bind({});
|
||||||
Primary.args = {
|
Primary.args = {
|
||||||
children: 'curl -X GET http://ultra-sound-money.eth',
|
text: 'curl -X GET http://ultra-sound-money.eth',
|
||||||
|
showCopyButton: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiLineWithChildren: Story<PropsWithChildren<unknown>> =
|
export const MultiLine: Story<Args> = Template.bind({});
|
||||||
Template.bind({});
|
MultiLine.args = {
|
||||||
MultiLineWithChildren.args = {
|
text: 'curl -X\n GET http://example-with-children.crypto',
|
||||||
children: 'curl -X\n GET http://example-with-children.crypto',
|
};
|
||||||
|
|
||||||
|
export const MultiLineWithIcon: Story<Args> = Template.bind({});
|
||||||
|
MultiLineWithIcon.args = {
|
||||||
|
text: 'curl -X\n GET http://example-with-children.crypto',
|
||||||
|
showCopyButton: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,33 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { useCopy } from '../Button/CopyButton/useCopy';
|
||||||
|
|
||||||
import styles from './Code.module.css';
|
import styles from './Code.module.css';
|
||||||
|
|
||||||
export function Code({ children }: PropsWithChildren<unknown>) {
|
interface Props {
|
||||||
return <code className={styles.code}>{children}</code>;
|
showCopyButton?: boolean;
|
||||||
|
children: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Code({ children, showCopyButton }: Props) {
|
||||||
|
const { handleCopy, copiedSuccessfully } = useCopy(children);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<code className={styles.code}>{children}</code>
|
||||||
|
|
||||||
|
{showCopyButton && (
|
||||||
|
<Button color="link" className={styles.copyButton} onClick={handleCopy}>
|
||||||
|
<i
|
||||||
|
className={clsx(
|
||||||
|
'fa',
|
||||||
|
copiedSuccessfully ? 'fa-check green-icon' : 'fa-copy '
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
32
app/portainer/components/NavTabs/NavTabs.module.css
Normal file
32
app/portainer/components/NavTabs/NavTabs.module.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.parent {
|
||||||
|
background: var(--blue-4);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
padding-top: 5px;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent a {
|
||||||
|
background-color: initial !important;
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
cursor: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent {
|
||||||
|
background-color: var(--blue-4);
|
||||||
|
}
|
||||||
|
:global(:root[theme='dark']) .parent {
|
||||||
|
background-color: var(--grey-40);
|
||||||
|
}
|
||||||
|
:global(:root[theme='highcontrast']) .parent {
|
||||||
|
background-color: var(--white-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent a {
|
||||||
|
color: var(--white-color) !important;
|
||||||
|
}
|
||||||
|
:global([theme='dark']) .parent a {
|
||||||
|
color: var(--black-color) !important;
|
||||||
|
}
|
||||||
|
:global([theme='highcontrast']) .parent a {
|
||||||
|
color: var(--black-color) !important;
|
||||||
|
}
|
31
app/portainer/components/NavTabs/NavTabs.stories.tsx
Normal file
31
app/portainer/components/NavTabs/NavTabs.stories.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { ComponentMeta, Story } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { NavTabs, type Option } from './NavTabs';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/NavTabs',
|
||||||
|
component: NavTabs,
|
||||||
|
} as ComponentMeta<typeof NavTabs>;
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
options: Option[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function Template({ options = [] }: Args) {
|
||||||
|
const [selected, setSelected] = useState(
|
||||||
|
options.length ? options[0].id : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavTabs options={options} selectedId={selected} onSelect={setSelected} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example: Story<Args> = Template.bind({});
|
||||||
|
Example.args = {
|
||||||
|
options: [
|
||||||
|
{ children: 'Content 1', id: 'option1', label: 'Option 1' },
|
||||||
|
{ children: 'Content 2', id: 'option2', label: 'Option 2' },
|
||||||
|
],
|
||||||
|
};
|
58
app/portainer/components/NavTabs/NavTabs.test.tsx
Normal file
58
app/portainer/components/NavTabs/NavTabs.test.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { NavTabs, Option } from './NavTabs';
|
||||||
|
|
||||||
|
test('should show titles', async () => {
|
||||||
|
const options = [
|
||||||
|
{ children: 'Content 1', id: 'option1', label: 'Option 1' },
|
||||||
|
{ children: 'Content 2', id: 'option2', label: 'Option 2' },
|
||||||
|
];
|
||||||
|
const { findByText } = renderComponent(options);
|
||||||
|
|
||||||
|
const heading = await findByText(options[0].label);
|
||||||
|
expect(heading).toBeTruthy();
|
||||||
|
|
||||||
|
const heading2 = await findByText(options[1].label);
|
||||||
|
expect(heading2).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show selected id content', async () => {
|
||||||
|
const options = [
|
||||||
|
{ children: 'Content 1', id: 'option1', label: 'Option 1' },
|
||||||
|
{ children: 'Content 2', id: 'option2', label: 'Option 2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selected = options[1];
|
||||||
|
|
||||||
|
const { findByText } = renderComponent(options, selected.id);
|
||||||
|
|
||||||
|
const content = await findByText(selected.children);
|
||||||
|
expect(content).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call onSelect when clicked with id', async () => {
|
||||||
|
const options = [
|
||||||
|
{ children: 'Content 1', id: 'option1', label: 'Option 1' },
|
||||||
|
{ children: 'Content 2', id: 'option2', label: 'Option 2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onSelect = jest.fn();
|
||||||
|
|
||||||
|
const { findByText } = renderComponent(options, options[1].id, onSelect);
|
||||||
|
|
||||||
|
const heading = await findByText(options[0].label);
|
||||||
|
userEvent.click(heading);
|
||||||
|
|
||||||
|
expect(onSelect).toHaveBeenCalledWith(options[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent(
|
||||||
|
options: Option[] = [],
|
||||||
|
selectedId?: string | number,
|
||||||
|
onSelect?: (id: string | number) => void
|
||||||
|
) {
|
||||||
|
return render(
|
||||||
|
<NavTabs options={options} selectedId={selectedId} onSelect={onSelect} />
|
||||||
|
);
|
||||||
|
}
|
60
app/portainer/components/NavTabs/NavTabs.tsx
Normal file
60
app/portainer/components/NavTabs/NavTabs.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from './NavTabs.module.css';
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
label: string | ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
id: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: Option[];
|
||||||
|
selectedId?: string | number;
|
||||||
|
onSelect?(id: string | number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavTabs({ options, selectedId, onSelect = () => {} }: Props) {
|
||||||
|
const selected = options.find((option) => option.id === selectedId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
{options.map((option) => (
|
||||||
|
<li
|
||||||
|
className={clsx({
|
||||||
|
active: option.id === selectedId,
|
||||||
|
[styles.parent]: !option.children,
|
||||||
|
})}
|
||||||
|
key={option.id}
|
||||||
|
>
|
||||||
|
{/* rule disabled because `nav-tabs` requires an anchor */}
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a
|
||||||
|
onClick={() => handleSelect(option)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
handleSelect(option);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{selected && selected.children && (
|
||||||
|
<div className="tab-content">{selected.children}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSelect(option: Option) {
|
||||||
|
if (option.children) {
|
||||||
|
onSelect(option.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { ButtonGroup, Size } from '../../Button/ButtonGroup';
|
import { ButtonGroup, Size } from '../../Button/ButtonGroup';
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import styles from './ButtonSelector.module.css';
|
||||||
|
|
||||||
export interface Option<T> {
|
export interface Option<T> {
|
||||||
value: T;
|
value: T;
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
|
|
16
app/portainer/environments/environment.service/edge.ts
Normal file
16
app/portainer/environments/environment.service/edge.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
interface GenerateUrlResponse {
|
||||||
|
edgeKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateKey() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post<GenerateUrlResponse>(
|
||||||
|
`/endpoints/edge/generate-key`
|
||||||
|
);
|
||||||
|
return data.edgeKey;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to generate key');
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ export function SettingsViewModel(data) {
|
||||||
this.DisableTrustOnFirstConnect = data.DisableTrustOnFirstConnect;
|
this.DisableTrustOnFirstConnect = data.DisableTrustOnFirstConnect;
|
||||||
this.EnforceEdgeID = data.EnforceEdgeID;
|
this.EnforceEdgeID = data.EnforceEdgeID;
|
||||||
this.AgentSecret = data.AgentSecret;
|
this.AgentSecret = data.AgentSecret;
|
||||||
|
this.EdgePortainerUrl = data.EdgePortainerUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicSettingsViewModel(settings) {
|
export function PublicSettingsViewModel(settings) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '../axios';
|
import axios, { parseAxiosError } from '../axios';
|
||||||
|
|
||||||
interface NodesCountResponse {
|
interface NodesCountResponse {
|
||||||
|
@ -13,6 +15,27 @@ export async function getNodesCount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StatusResponse {
|
||||||
|
Version: string;
|
||||||
|
InstanceID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStatus() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<StatusResponse>(buildUrl());
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStatus<T = StatusResponse>(
|
||||||
|
select?: (status: StatusResponse) => T
|
||||||
|
) {
|
||||||
|
return useQuery(['status'], () => getStatus(), { select });
|
||||||
|
}
|
||||||
|
|
||||||
function buildUrl(action?: string) {
|
function buildUrl(action?: string) {
|
||||||
let url = '/status';
|
let url = '/status';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||||
|
import { EdgeScriptForm } from '@/edge/components/EdgeScriptForm';
|
||||||
|
import { generateKey } from '@/portainer/environments/environment.service/edge';
|
||||||
|
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||||
|
|
||||||
|
export function AutomaticEdgeEnvCreation() {
|
||||||
|
const edgeKeyMutation = useGenerateKeyMutation();
|
||||||
|
const { mutate } = edgeKeyMutation;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [mutate]);
|
||||||
|
|
||||||
|
const edgeKey = edgeKeyMutation.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget>
|
||||||
|
<WidgetTitle
|
||||||
|
icon="fa-laptop"
|
||||||
|
title="Automatic Edge Environment Creation"
|
||||||
|
/>
|
||||||
|
<WidgetBody>
|
||||||
|
{edgeKey ? (
|
||||||
|
<EdgeScriptForm edgeKey={edgeKey} />
|
||||||
|
) : (
|
||||||
|
<TextTip>Please choose a valid edge portainer URL</TextTip>
|
||||||
|
)}
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// using mutation because this action generates an object (although it's not saved in db)
|
||||||
|
function useGenerateKeyMutation() {
|
||||||
|
return useMutation(generateKey);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
|
@ -1,4 +1,4 @@
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form, Field } from 'formik';
|
||||||
|
|
||||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||||
|
@ -6,19 +6,24 @@ import { Select } from '@/portainer/components/form-components/Input/Select';
|
||||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||||
|
import { Input } from '@/portainer/components/form-components/Input';
|
||||||
|
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||||
|
|
||||||
import styles from './SettingsEdgeCompute.module.css';
|
import { Settings } from '../types';
|
||||||
import { validationSchema } from './SettingsEdgeCompute.validation';
|
|
||||||
|
import styles from './EdgeComputeSettings.module.css';
|
||||||
|
import { validationSchema } from './EdgeComputeSettings.validation';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
EdgeAgentCheckinInterval: number;
|
EdgeAgentCheckinInterval: number;
|
||||||
EnableEdgeComputeFeatures: boolean;
|
EnableEdgeComputeFeatures: boolean;
|
||||||
DisableTrustOnFirstConnect: boolean;
|
DisableTrustOnFirstConnect: boolean;
|
||||||
EnforceEdgeID: boolean;
|
EnforceEdgeID: boolean;
|
||||||
|
EdgePortainerUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
settings: FormValues;
|
settings?: Settings;
|
||||||
onSubmit(values: FormValues): void;
|
onSubmit(values: FormValues): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,16 +42,17 @@ const checkinIntervalOptions = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||||
const initialValues = {
|
if (!settings) {
|
||||||
EdgeAgentCheckinInterval: settings ? settings.EdgeAgentCheckinInterval : 5,
|
return null;
|
||||||
EnableEdgeComputeFeatures: settings
|
}
|
||||||
? settings.EnableEdgeComputeFeatures
|
|
||||||
: false,
|
const initialValues: FormValues = {
|
||||||
DisableTrustOnFirstConnect: settings
|
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||||
? settings.DisableTrustOnFirstConnect
|
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||||
: false,
|
DisableTrustOnFirstConnect: settings.DisableTrustOnFirstConnect,
|
||||||
EnforceEdgeID: settings ? settings.EnforceEdgeID : false,
|
EnforceEdgeID: settings.EnforceEdgeID,
|
||||||
|
EdgePortainerUrl: settings.EdgePortainerUrl || buildDefaultUrl(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -133,6 +139,16 @@ export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
label="Portainer URL"
|
||||||
|
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
|
||||||
|
inputId="url-input"
|
||||||
|
errors={errors.EdgePortainerUrl}
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
<Field as={Input} id="url-input" name="EdgePortainerUrl" />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
@ -154,3 +170,8 @@ export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaultUrl() {
|
||||||
|
const base = baseHref();
|
||||||
|
return window.location.origin + (base !== '/' ? base : '');
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { boolean, number, object } from 'yup';
|
import { boolean, number, object, string } from 'yup';
|
||||||
|
|
||||||
export function validationSchema() {
|
export function validationSchema() {
|
||||||
return object().shape({
|
return object().shape({
|
||||||
|
@ -6,5 +6,13 @@ export function validationSchema() {
|
||||||
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
||||||
DisableTrustOnFirstConnect: boolean().required('This field is required.'),
|
DisableTrustOnFirstConnect: boolean().required('This field is required.'),
|
||||||
EnforceEdgeID: boolean().required('This field is required.'),
|
EnforceEdgeID: boolean().required('This field is required.'),
|
||||||
|
EdgePortainerUrl: string()
|
||||||
|
.test(
|
||||||
|
'notlocal',
|
||||||
|
'Cannot use localhost as environment URL',
|
||||||
|
(value) => !value?.includes('localhost')
|
||||||
|
)
|
||||||
|
.url('URL should be a valid URI')
|
||||||
|
.required('URL is required'),
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { EdgeComputeSettings } from './EdgeComputeSettings';
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { EdgeComputeSettings } from './EdgeComputeSettings';
|
||||||
|
import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
||||||
|
import { Settings } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settings: Settings;
|
||||||
|
onSubmit(values: Settings): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EdgeComputeSettingsView({ settings, onSubmit }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<EdgeComputeSettings settings={settings} onSubmit={onSubmit} />
|
||||||
|
|
||||||
|
{process.env.PORTAINER_EDITION === 'BE' && <AutomaticEdgeEnvCreation />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EdgeComputeSettingsViewAngular = r2a(EdgeComputeSettingsView, [
|
||||||
|
'settings',
|
||||||
|
'onSubmit',
|
||||||
|
]);
|
|
@ -1,9 +0,0 @@
|
||||||
import { react2angular } from '@/react-tools/react2angular';
|
|
||||||
|
|
||||||
import { SettingsEdgeCompute } from './SettingsEdgeCompute';
|
|
||||||
|
|
||||||
const SettingsEdgeComputeAngular = react2angular(SettingsEdgeCompute, [
|
|
||||||
'settings',
|
|
||||||
'onSubmit',
|
|
||||||
]);
|
|
||||||
export { SettingsEdgeCompute, SettingsEdgeComputeAngular };
|
|
7
app/portainer/settings/edge-compute/types.ts
Normal file
7
app/portainer/settings/edge-compute/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Settings {
|
||||||
|
EdgeAgentCheckinInterval: number;
|
||||||
|
EnableEdgeComputeFeatures: boolean;
|
||||||
|
DisableTrustOnFirstConnect: boolean;
|
||||||
|
EnforceEdgeID: boolean;
|
||||||
|
EdgePortainerUrl: string;
|
||||||
|
}
|
|
@ -3,12 +3,12 @@ import angular from 'angular';
|
||||||
import authenticationModule from './authentication';
|
import authenticationModule from './authentication';
|
||||||
import generalModule from './general';
|
import generalModule from './general';
|
||||||
|
|
||||||
import { SettingsEdgeComputeAngular } from './edge-compute/SettingsEdgeCompute';
|
|
||||||
import { SettingsFDOAngular } from './edge-compute/SettingsFDO';
|
import { SettingsFDOAngular } from './edge-compute/SettingsFDO';
|
||||||
import { SettingsOpenAMTAngular } from './edge-compute/SettingsOpenAMT';
|
import { SettingsOpenAMTAngular } from './edge-compute/SettingsOpenAMT';
|
||||||
|
import { EdgeComputeSettingsViewAngular } from './edge-compute/EdgeComputeSettingsView';
|
||||||
|
|
||||||
export default angular
|
export default angular
|
||||||
.module('portainer.settings', [authenticationModule, generalModule])
|
.module('portainer.settings', [authenticationModule, generalModule])
|
||||||
.component('settingsEdgeCompute', SettingsEdgeComputeAngular)
|
.component('settingsEdgeCompute', EdgeComputeSettingsViewAngular)
|
||||||
.component('settingsFdo', SettingsFDOAngular)
|
.component('settingsFdo', SettingsFDOAngular)
|
||||||
.component('settingsOpenAmt', SettingsOpenAMTAngular).name;
|
.component('settingsOpenAmt', SettingsOpenAMTAngular).name;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '../services/axios';
|
import axios, { parseAxiosError } from '../services/axios';
|
||||||
|
@ -14,6 +16,51 @@ export async function publicSettings() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AuthenticationMethod {
|
||||||
|
// AuthenticationInternal represents the internal authentication method (authentication against Portainer API)
|
||||||
|
AuthenticationInternal,
|
||||||
|
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||||
|
AuthenticationLDAP,
|
||||||
|
// AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server)
|
||||||
|
AuthenticationOAuth,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsResponse {
|
||||||
|
LogoURL: string;
|
||||||
|
BlackListedLabels: { name: string; value: string }[];
|
||||||
|
AuthenticationMethod: AuthenticationMethod;
|
||||||
|
SnapshotInterval: string;
|
||||||
|
TemplatesURL: string;
|
||||||
|
EdgeAgentCheckinInterval: number;
|
||||||
|
EnableEdgeComputeFeatures: boolean;
|
||||||
|
UserSessionTimeout: string;
|
||||||
|
KubeconfigExpiry: string;
|
||||||
|
EnableTelemetry: boolean;
|
||||||
|
HelmRepositoryURL: string;
|
||||||
|
KubectlShellImage: string;
|
||||||
|
DisableTrustOnFirstConnect: boolean;
|
||||||
|
EnforceEdgeID: boolean;
|
||||||
|
AgentSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSettings() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<SettingsResponse>(buildUrl());
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(
|
||||||
|
e as Error,
|
||||||
|
'Unable to retrieve application settings'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettings<T = SettingsResponse>(
|
||||||
|
select?: (settings: SettingsResponse) => T
|
||||||
|
) {
|
||||||
|
return useQuery(['settings'], getSettings, { select });
|
||||||
|
}
|
||||||
|
|
||||||
function buildUrl(subResource?: string, action?: string) {
|
function buildUrl(subResource?: string, action?: string) {
|
||||||
let url = 'settings';
|
let url = 'settings';
|
||||||
if (subResource) {
|
if (subResource) {
|
||||||
|
|
|
@ -39,82 +39,45 @@
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
<information-panel ng-if="!state.edgeAssociated" title-text="Deploy an agent">
|
|
||||||
<span class="small text-muted">
|
<div class="col-sm-12" ng-if="!state.edgeAssociated">
|
||||||
<p>
|
<rd-widget>
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
<rd-widget-body>
|
||||||
Refer to the platform related command below to deploy the Edge agent in your remote cluster.
|
<div class="col-sm-12 form-section-title">Deploy an agent</div>
|
||||||
</p>
|
<span class="small text-muted">
|
||||||
<p>
|
<p>
|
||||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
</p>
|
Refer to the platform related command below to deploy the Edge agent in your remote cluster.
|
||||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px">
|
</p>
|
||||||
<div class="btn-group btn-group-sm">
|
<p>
|
||||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'linux'"><i class="fab fa-linux" style="margin-right: 2px"></i> Linux</label>
|
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px"></i> Windows</label>
|
</p>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<edge-script-form edge-key="endpoint.EdgeKey" edge-id="endpoint.EdgeID"></edge-script-form>
|
||||||
<por-switch-field
|
|
||||||
label="'Allow self-signed certs'"
|
<span class="small text-muted">
|
||||||
checked="state.allowSelfSignedCerts"
|
<div class="col-sm-12 form-section-title" style="margin-top: 25px"> Join token </div>
|
||||||
tooltip="'When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS'"
|
<p>
|
||||||
on-change="(onToggleAllowSelfSignedCerts)"
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
></por-switch-field>
|
For those prestaging the edge agent, use the following join token to associate the Edge agent with this environment.
|
||||||
</div>
|
</p>
|
||||||
<div class="form-group clearfix" ng-if="!isKubernetesDeploymentTabSelected()">
|
<p> You can read more about pre-staging in the userguide available <a href="https://downloads.portainer.io/edge_agent_guide.pdf">here.</a> </p>
|
||||||
<label for="env_vars" class="col-sm-3 col-lg-2 control-label text-left" style="padding-left: 0; padding-top: 5px">
|
<div style="margin-top: 10px; overflow-wrap: break-word">
|
||||||
Environment variables
|
<code>
|
||||||
<portainer-tooltip
|
{{ endpoint.EdgeKey }}
|
||||||
position="bottom"
|
</code>
|
||||||
message="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
|
<div style="margin-top: 10px">
|
||||||
></portainer-tooltip>
|
<span class="btn btn-primary btn-sm" ng-click="copyEdgeAgentKey()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy token</span>
|
||||||
</label>
|
<span id="copyNotificationEdgeKey" style="margin-left: 7px; display: none; color: #23ae89"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
||||||
<div class="col-sm-9 col-lg-10">
|
</div>
|
||||||
<input type="text" class="form-control" id="env_vars" ng-model="formValues.EnvVarSource" placeholder="foo=bar,myvar" />
|
</div>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</rd-widget-body>
|
||||||
<div style="margin-top: 10px">
|
</rd-widget>
|
||||||
<uib-tabset active="state.deploymentTab">
|
</div>
|
||||||
<uib-tab index="'kubernetes'" heading="Kubernetes" ng-if="state.platformType === 'linux'">
|
|
||||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
|
||||||
dockerCommands[state.deploymentTab][state.platformType](agentShortVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
|
||||||
}}</code>
|
|
||||||
</uib-tab>
|
|
||||||
<uib-tab index="'swarm'" heading="Docker Swarm">
|
|
||||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
|
||||||
dockerCommands[state.deploymentTab][state.platformType](agentVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
|
||||||
}}</code>
|
|
||||||
</uib-tab>
|
|
||||||
<uib-tab index="'standalone'" heading="Docker Standalone">
|
|
||||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px">{{
|
|
||||||
dockerCommands[state.deploymentTab][state.platformType](agentVersion, endpoint.EdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts)
|
|
||||||
}}</code>
|
|
||||||
</uib-tab>
|
|
||||||
</uib-tabset>
|
|
||||||
<div style="margin-top: 10px">
|
|
||||||
<span class="btn btn-primary btn-sm" ng-click="copyEdgeAgentDeploymentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy command</span>
|
|
||||||
<span id="copyNotificationDeploymentCommand" style="margin-left: 7px; display: none; color: #23ae89"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 form-section-title" style="margin-top: 25px"> Join token </div>
|
|
||||||
<p>
|
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
|
||||||
For those prestaging the edge agent, use the following join token to associate the Edge agent with this environment.
|
|
||||||
</p>
|
|
||||||
<p> You can read more about pre-staging in the userguide available <a href="https://downloads.portainer.io/edge_agent_guide.pdf">here.</a> </p>
|
|
||||||
<div style="margin-top: 10px; overflow-wrap: break-word">
|
|
||||||
<code>
|
|
||||||
{{ endpoint.EdgeKey }}
|
|
||||||
</code>
|
|
||||||
<div style="margin-top: 10px">
|
|
||||||
<span class="btn btn-primary btn-sm" ng-click="copyEdgeAgentKey()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy token</span>
|
|
||||||
<span id="copyNotificationEdgeKey" style="margin-left: 7px; display: none; color: #23ae89"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</information-panel>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
|
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
|
||||||
<span class="small text-muted">
|
<span class="small text-muted">
|
||||||
<i class="fa fa-tools blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
<i class="fa fa-tools blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import uuidv4 from 'uuid/v4';
|
||||||
|
|
||||||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||||
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||||
import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
|
|
||||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||||
import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||||
|
|
||||||
|
@ -23,25 +22,11 @@ function EndpointController(
|
||||||
Notifications,
|
Notifications,
|
||||||
Authentication,
|
Authentication,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
ModalService,
|
ModalService
|
||||||
StateManager
|
|
||||||
) {
|
) {
|
||||||
const DEPLOYMENT_TABS = {
|
|
||||||
SWARM: 'swarm',
|
|
||||||
STANDALONE: 'standalone',
|
|
||||||
KUBERNETES: 'kubernetes',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PLATFORM_TYPES = {
|
|
||||||
WINDOWS: 'windows',
|
|
||||||
LINUX: 'linux',
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
uploadInProgress: false,
|
uploadInProgress: false,
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
deploymentTab: DEPLOYMENT_TABS.KUBERNETES,
|
|
||||||
platformType: PLATFORM_TYPES.LINUX,
|
|
||||||
azureEndpoint: false,
|
azureEndpoint: false,
|
||||||
kubernetesEndpoint: false,
|
kubernetesEndpoint: false,
|
||||||
agentEndpoint: false,
|
agentEndpoint: false,
|
||||||
|
@ -70,51 +55,10 @@ function EndpointController(
|
||||||
showAMTInfo: false,
|
showAMTInfo: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.agentVersion = StateManager.getState().application.version;
|
|
||||||
$scope.agentShortVersion = getAgentShortVersion($scope.agentVersion);
|
|
||||||
$scope.agentSecret = '';
|
|
||||||
|
|
||||||
$scope.dockerCommands = {
|
|
||||||
[DEPLOYMENT_TABS.STANDALONE]: {
|
|
||||||
[PLATFORM_TYPES.LINUX]: buildLinuxStandaloneCommand,
|
|
||||||
[PLATFORM_TYPES.WINDOWS]: buildWindowsStandaloneCommand,
|
|
||||||
},
|
|
||||||
[DEPLOYMENT_TABS.SWARM]: {
|
|
||||||
[PLATFORM_TYPES.LINUX]: buildLinuxSwarmCommand,
|
|
||||||
[PLATFORM_TYPES.WINDOWS]: buildWindowsSwarmCommand,
|
|
||||||
},
|
|
||||||
[DEPLOYMENT_TABS.KUBERNETES]: {
|
|
||||||
[PLATFORM_TYPES.LINUX]: buildKubernetesCommand,
|
|
||||||
[PLATFORM_TYPES.WINDOWS]: () => '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
SecurityFormData: new EndpointSecurityFormData(),
|
SecurityFormData: new EndpointSecurityFormData(),
|
||||||
EnvVarSource: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isKubernetesDeploymentTabSelected = function () {
|
|
||||||
return $scope.state.deploymentTab === DEPLOYMENT_TABS.KUBERNETES;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.copyEdgeAgentDeploymentCommand = copyEdgeAgentDeploymentCommand;
|
|
||||||
function copyEdgeAgentDeploymentCommand() {
|
|
||||||
let agentVersion = $scope.agentVersion;
|
|
||||||
if ($scope.state.deploymentTab == DEPLOYMENT_TABS.KUBERNETES) {
|
|
||||||
agentVersion = $scope.agentShortVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = $scope.dockerCommands[$scope.state.deploymentTab][$scope.state.platformType](
|
|
||||||
agentVersion,
|
|
||||||
$scope.endpoint.EdgeID,
|
|
||||||
$scope.endpoint.EdgeKey,
|
|
||||||
$scope.state.allowSelfSignedCerts
|
|
||||||
);
|
|
||||||
clipboard.copyText(command.trim());
|
|
||||||
$('#copyNotificationDeploymentCommand').show().fadeOut(2500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.copyEdgeAgentKey = function () {
|
$scope.copyEdgeAgentKey = function () {
|
||||||
clipboard.copyText($scope.endpoint.EdgeKey);
|
clipboard.copyText($scope.endpoint.EdgeKey);
|
||||||
$('#copyNotificationEdgeKey').show().fadeOut(2500);
|
$('#copyNotificationEdgeKey').show().fadeOut(2500);
|
||||||
|
@ -291,7 +235,6 @@ function EndpointController(
|
||||||
$scope.endpoint = endpoint;
|
$scope.endpoint = endpoint;
|
||||||
$scope.groups = groups;
|
$scope.groups = groups;
|
||||||
$scope.availableTags = tags;
|
$scope.availableTags = tags;
|
||||||
$scope.agentSecret = settings.AgentSecret;
|
|
||||||
|
|
||||||
configureState();
|
configureState();
|
||||||
|
|
||||||
|
@ -326,126 +269,5 @@ function EndpointController(
|
||||||
$scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
|
$scope.endpoint.ManagementInfo['DNS Suffix'] = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEnvironmentSubCommand() {
|
|
||||||
let env = [];
|
|
||||||
if ($scope.formValues.EnvVarSource != '') {
|
|
||||||
env = $scope.formValues.EnvVarSource.split(',')
|
|
||||||
.map(function (s) {
|
|
||||||
if (s !== '') {
|
|
||||||
return `-e ${s} \\`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((s) => s !== undefined);
|
|
||||||
}
|
|
||||||
if ($scope.agentSecret != '') {
|
|
||||||
env.push(`-e AGENT_SECRET=${$scope.agentSecret} \\`);
|
|
||||||
}
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLinuxStandaloneCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
|
||||||
const env = buildEnvironmentSubCommand();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'docker run -d \\',
|
|
||||||
'-v /var/run/docker.sock:/var/run/docker.sock \\',
|
|
||||||
'-v /var/lib/docker/volumes:/var/lib/docker/volumes \\',
|
|
||||||
'-v /:/host \\',
|
|
||||||
'-v portainer_agent_data:/data \\',
|
|
||||||
'--restart always \\',
|
|
||||||
'-e EDGE=1 \\',
|
|
||||||
`-e EDGE_ID=${edgeId} \\`,
|
|
||||||
`-e EDGE_KEY=${edgeKey} \\`,
|
|
||||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
|
||||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
|
||||||
...env,
|
|
||||||
'--name portainer_edge_agent \\',
|
|
||||||
`portainer/agent:${agentVersion}`,
|
|
||||||
].join('\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWindowsStandaloneCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
|
||||||
const env = buildEnvironmentSubCommand();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'docker run -d \\',
|
|
||||||
'--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\',
|
|
||||||
'--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\',
|
|
||||||
'--mount type=volume,src=portainer_agent_data,dst=C:\\data \\',
|
|
||||||
'--restart always \\',
|
|
||||||
'-e EDGE=1 \\',
|
|
||||||
`-e EDGE_ID=${edgeId} \\`,
|
|
||||||
`-e EDGE_KEY=${edgeKey} \\`,
|
|
||||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
|
||||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
|
||||||
...env,
|
|
||||||
'--name portainer_edge_agent \\',
|
|
||||||
`portainer/agent:${agentVersion}`,
|
|
||||||
].join('\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLinuxSwarmCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
|
||||||
const env = buildEnvironmentSubCommand();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'docker network create \\',
|
|
||||||
'--driver overlay \\',
|
|
||||||
'portainer_agent_network;',
|
|
||||||
'',
|
|
||||||
|
|
||||||
'docker service create \\',
|
|
||||||
'--name portainer_edge_agent \\',
|
|
||||||
'--network portainer_agent_network \\',
|
|
||||||
'-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\',
|
|
||||||
'-e EDGE=1 \\',
|
|
||||||
`-e EDGE_ID=${edgeId} \\`,
|
|
||||||
`-e EDGE_KEY=${edgeKey} \\`,
|
|
||||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
|
||||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
|
||||||
...env,
|
|
||||||
'--mode global \\',
|
|
||||||
"--constraint 'node.platform.os == linux' \\",
|
|
||||||
'--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\',
|
|
||||||
'--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\',
|
|
||||||
'--mount type=bind,src=//,dst=/host \\',
|
|
||||||
'--mount type=volume,src=portainer_agent_data,dst=/data \\',
|
|
||||||
`portainer/agent:${agentVersion}`,
|
|
||||||
].join('\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWindowsSwarmCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
|
||||||
const env = buildEnvironmentSubCommand();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'docker network create \\',
|
|
||||||
'--driver overlay \\',
|
|
||||||
'portainer_agent_network;',
|
|
||||||
'',
|
|
||||||
|
|
||||||
'docker service create \\',
|
|
||||||
'--name portainer_edge_agent \\',
|
|
||||||
'--network portainer_agent_network \\',
|
|
||||||
'-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \\',
|
|
||||||
'-e EDGE=1 \\',
|
|
||||||
`-e EDGE_ID=${edgeId} \\`,
|
|
||||||
`-e EDGE_KEY=${edgeKey} \\`,
|
|
||||||
'-e CAP_HOST_MANAGEMENT=1 \\',
|
|
||||||
`-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\`,
|
|
||||||
...env,
|
|
||||||
'--mode global \\',
|
|
||||||
"--constraint 'node.platform.os == windows' \\",
|
|
||||||
'--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\',
|
|
||||||
'--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\',
|
|
||||||
'--mount type=volume,src=portainer_agent_data,dst=C:\\data \\',
|
|
||||||
`portainer/agent:${agentVersion}`,
|
|
||||||
].join('\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildKubernetesCommand(agentVersion, edgeId, edgeKey, allowSelfSignedCerts) {
|
|
||||||
return `curl https://downloads.portainer.io/portainer-ce${agentVersion}-edge-agent-setup.sh | bash -s -- ${edgeId} ${edgeKey} ${allowSelfSignedCerts ? '1' : '0'} ${
|
|
||||||
$scope.agentSecret
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue