mirror of https://github.com/portainer/portainer
refactor(edge): move edge deploy script to react [EE-2689] (#6747)
parent
328ce2f995
commit
85a7b7e0fc
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
// 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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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']);
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}` : ``,
|
||||
]);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { EdgeScriptForm, EdgeScriptFormAngular } from './EdgeScriptForm';
|
|
@ -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;
|
||||
}
|
|
@ -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 { Button } from '../Button';
|
||||
|
||||
import styles from './CopyButton.module.css';
|
||||
import { useCopy } from './useCopy';
|
||||
|
||||
export interface Props {
|
||||
copyText: string;
|
||||
|
@ -19,49 +20,29 @@ export function CopyButton({
|
|||
className,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const [isFading, setIsFading] = useState(false);
|
||||
|
||||
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);
|
||||
}
|
||||
const { handleCopy, copiedSuccessfully } = useCopy(copyText, fadeDelay);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={className}
|
||||
size="small"
|
||||
onClick={onClick}
|
||||
onClick={handleCopy}
|
||||
title="Copy Value"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa fa-copy space-right" aria-hidden="true" /> {children}
|
||||
</Button>
|
||||
|
||||
<span className={clsx(isFading && styles.fadeout, styles.displayText)}>
|
||||
<i className="fa fa-check" aria-hidden="true" /> {displayText}
|
||||
<span
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
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 { PropsWithChildren } from 'react';
|
||||
|
||||
import { Code } from './Code';
|
||||
|
||||
|
@ -8,19 +7,28 @@ export default {
|
|||
title: 'Components/Code',
|
||||
} as Meta;
|
||||
|
||||
function Template({
|
||||
children,
|
||||
}: JSX.IntrinsicAttributes & PropsWithChildren<unknown>) {
|
||||
return <Code>{children}</Code>;
|
||||
interface Args {
|
||||
text: string;
|
||||
showCopyButton?: boolean;
|
||||
}
|
||||
|
||||
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 = {
|
||||
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>> =
|
||||
Template.bind({});
|
||||
MultiLineWithChildren.args = {
|
||||
children: 'curl -X\n GET http://example-with-children.crypto',
|
||||
export const MultiLine: Story<Args> = Template.bind({});
|
||||
MultiLine.args = {
|
||||
text: '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';
|
||||
|
||||
export function Code({ children }: PropsWithChildren<unknown>) {
|
||||
return <code className={styles.code}>{children}</code>;
|
||||
interface Props {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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' },
|
||||
],
|
||||
};
|
|
@ -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} />
|
||||
);
|
||||
}
|
|
@ -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 { PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { ButtonGroup, Size } from '../../Button/ButtonGroup';
|
||||
|
||||
|
@ -7,7 +7,7 @@ import styles from './ButtonSelector.module.css';
|
|||
|
||||
export interface Option<T> {
|
||||
value: T;
|
||||
label?: string;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
|
|
|
@ -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.EnforceEdgeID = data.EnforceEdgeID;
|
||||
this.AgentSecret = data.AgentSecret;
|
||||
this.EdgePortainerUrl = data.EdgePortainerUrl;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '../axios';
|
||||
|
||||
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) {
|
||||
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 { 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 { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
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 { validationSchema } from './SettingsEdgeCompute.validation';
|
||||
import { Settings } from '../types';
|
||||
|
||||
import styles from './EdgeComputeSettings.module.css';
|
||||
import { validationSchema } from './EdgeComputeSettings.validation';
|
||||
|
||||
export interface FormValues {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
DisableTrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: FormValues;
|
||||
settings?: Settings;
|
||||
onSubmit(values: FormValues): void;
|
||||
}
|
||||
|
||||
|
@ -37,16 +42,17 @@ const checkinIntervalOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
||||
const initialValues = {
|
||||
EdgeAgentCheckinInterval: settings ? settings.EdgeAgentCheckinInterval : 5,
|
||||
EnableEdgeComputeFeatures: settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false,
|
||||
DisableTrustOnFirstConnect: settings
|
||||
? settings.DisableTrustOnFirstConnect
|
||||
: false,
|
||||
EnforceEdgeID: settings ? settings.EnforceEdgeID : false,
|
||||
export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValues: FormValues = {
|
||||
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
DisableTrustOnFirstConnect: settings.DisableTrustOnFirstConnect,
|
||||
EnforceEdgeID: settings.EnforceEdgeID,
|
||||
EdgePortainerUrl: settings.EdgePortainerUrl || buildDefaultUrl(),
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -133,6 +139,16 @@ export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
|||
/>
|
||||
</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="col-sm-12">
|
||||
<LoadingButton
|
||||
|
@ -154,3 +170,8 @@ export function SettingsEdgeCompute({ settings, onSubmit }: Props) {
|
|||
</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() {
|
||||
return object().shape({
|
||||
|
@ -6,5 +6,13 @@ export function validationSchema() {
|
|||
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
||||
DisableTrustOnFirstConnect: 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 };
|
|
@ -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 generalModule from './general';
|
||||
|
||||
import { SettingsEdgeComputeAngular } from './edge-compute/SettingsEdgeCompute';
|
||||
import { SettingsFDOAngular } from './edge-compute/SettingsFDO';
|
||||
import { SettingsOpenAMTAngular } from './edge-compute/SettingsOpenAMT';
|
||||
import { EdgeComputeSettingsViewAngular } from './edge-compute/EdgeComputeSettingsView';
|
||||
|
||||
export default angular
|
||||
.module('portainer.settings', [authenticationModule, generalModule])
|
||||
.component('settingsEdgeCompute', SettingsEdgeComputeAngular)
|
||||
.component('settingsEdgeCompute', EdgeComputeSettingsViewAngular)
|
||||
.component('settingsFdo', SettingsFDOAngular)
|
||||
.component('settingsOpenAmt', SettingsOpenAMTAngular).name;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
|
||||
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) {
|
||||
let url = 'settings';
|
||||
if (subResource) {
|
||||
|
|
|
@ -39,82 +39,45 @@
|
|||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
<information-panel ng-if="!state.edgeAssociated" title-text="Deploy an agent">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Refer to the platform related command below to deploy the Edge agent in your remote cluster.
|
||||
</p>
|
||||
<p>
|
||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||
</p>
|
||||
<div class="input-group input-group-sm" style="margin-top: 10px; margin-bottom: 10px">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<por-switch-field
|
||||
label="'Allow self-signed certs'"
|
||||
checked="state.allowSelfSignedCerts"
|
||||
tooltip="'When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS'"
|
||||
on-change="(onToggleAllowSelfSignedCerts)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
<div class="form-group clearfix" ng-if="!isKubernetesDeploymentTabSelected()">
|
||||
<label for="env_vars" class="col-sm-3 col-lg-2 control-label text-left" style="padding-left: 0; padding-top: 5px">
|
||||
Environment variables
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Comma separated list of environment variables that will be sourced from the host where the agent is deployed."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="env_vars" ng-model="formValues.EnvVarSource" placeholder="foo=bar,myvar" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<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 class="col-sm-12" ng-if="!state.edgeAssociated">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="col-sm-12 form-section-title">Deploy an agent</div>
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Refer to the platform related command below to deploy the Edge agent in your remote cluster.
|
||||
</p>
|
||||
<p>
|
||||
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<edge-script-form edge-key="endpoint.EdgeKey" edge-id="endpoint.EdgeID"></edge-script-form>
|
||||
|
||||
<span class="small text-muted">
|
||||
<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>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<information-panel ng-if="state.kubernetesEndpoint && (!state.edgeEndpoint || state.edgeAssociated)" title-text="Kubernetes features configuration">
|
||||
<span class="small text-muted">
|
||||
<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 { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||
import { getAgentShortVersion } from 'Portainer/views/endpoints/helpers';
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||
|
||||
|
@ -23,25 +22,11 @@ function EndpointController(
|
|||
Notifications,
|
||||
Authentication,
|
||||
SettingsService,
|
||||
ModalService,
|
||||
StateManager
|
||||
ModalService
|
||||
) {
|
||||
const DEPLOYMENT_TABS = {
|
||||
SWARM: 'swarm',
|
||||
STANDALONE: 'standalone',
|
||||
KUBERNETES: 'kubernetes',
|
||||
};
|
||||
|
||||
const PLATFORM_TYPES = {
|
||||
WINDOWS: 'windows',
|
||||
LINUX: 'linux',
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false,
|
||||
deploymentTab: DEPLOYMENT_TABS.KUBERNETES,
|
||||
platformType: PLATFORM_TYPES.LINUX,
|
||||
azureEndpoint: false,
|
||||
kubernetesEndpoint: false,
|
||||
agentEndpoint: false,
|
||||
|
@ -70,51 +55,10 @@ function EndpointController(
|
|||
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 = {
|
||||
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 () {
|
||||
clipboard.copyText($scope.endpoint.EdgeKey);
|
||||
$('#copyNotificationEdgeKey').show().fadeOut(2500);
|
||||
|
@ -291,7 +235,6 @@ function EndpointController(
|
|||
$scope.endpoint = endpoint;
|
||||
$scope.groups = groups;
|
||||
$scope.availableTags = tags;
|
||||
$scope.agentSecret = settings.AgentSecret;
|
||||
|
||||
configureState();
|
||||
|
||||
|
@ -326,126 +269,5 @@ function EndpointController(
|
|||
$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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue