refactor(edge): move edge deploy script to react [EE-2689] (#6747)

pull/6734/head
Chaim Lev-Ari 2022-04-14 13:14:23 +03:00 committed by GitHub
parent 328ce2f995
commit 85a7b7e0fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1079 additions and 342 deletions

View File

@ -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)

View File

@ -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 {

28
api/internal/edge/url.go Normal file
View 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
}

View File

@ -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

View File

@ -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',

View 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>
);
}

View 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']);

View 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>
);
}

View 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}` : ``,
]);
}

View File

@ -0,0 +1 @@
export { EdgeScriptForm, EdgeScriptFormAngular } from './EdgeScriptForm';

View 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;
}

View 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;

View File

@ -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>
);

View 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 };
}

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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>
);
}

View 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;
}

View 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' },
],
};

View 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} />
);
}

View 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);
}
}
}

View File

@ -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> {

View 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');
}
}

View File

@ -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) {

View File

@ -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';

View File

@ -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);
}

View File

@ -0,0 +1 @@
export { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';

View File

@ -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 : '');
}

View File

@ -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'),
});
}

View File

@ -0,0 +1 @@
export { EdgeComputeSettings } from './EdgeComputeSettings';

View File

@ -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',
]);

View File

@ -1,9 +0,0 @@
import { react2angular } from '@/react-tools/react2angular';
import { SettingsEdgeCompute } from './SettingsEdgeCompute';
const SettingsEdgeComputeAngular = react2angular(SettingsEdgeCompute, [
'settings',
'onSubmit',
]);
export { SettingsEdgeCompute, SettingsEdgeComputeAngular };

View File

@ -0,0 +1,7 @@
export interface Settings {
EdgeAgentCheckinInterval: number;
EnableEdgeComputeFeatures: boolean;
DisableTrustOnFirstConnect: boolean;
EnforceEdgeID: boolean;
EdgePortainerUrl: string;
}

View File

@ -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;

View File

@ -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) {

View File

@ -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>

View File

@ -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();
}