1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 07:49:41 +02:00

feat(wizard): add edge form [EE-3000] (#6979)

This commit is contained in:
Chaim Lev-Ari 2022-06-01 07:28:31 +03:00 committed by GitHub
parent e686d64011
commit ac096dda46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 793 additions and 316 deletions

View file

@ -0,0 +1,70 @@
import { Formik } from 'formik';
import { OsSelector } from './OsSelector';
import { CommandTab } from './scripts';
import { ScriptTabs } from './ScriptTabs';
import { EdgeScriptSettingsFieldset } from './EdgeScriptSettingsFieldset';
import { validationSchema } from './EdgeScriptForm.validation';
import { ScriptFormValues, OS, Platform, EdgeInfo } from './types';
const edgePropertiesFormInitialValues: ScriptFormValues = {
allowSelfSignedCertificates: true,
envVars: '',
os: 'linux' as OS,
platform: 'k8s' as Platform,
nomadToken: '',
authEnabled: true,
};
interface Props {
edgeInfo: EdgeInfo;
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
isNomadTokenVisible?: boolean;
}
export function EdgeScriptForm({
edgeInfo,
commands,
isNomadTokenVisible,
}: Props) {
const showOsSelector = !(commands instanceof Array);
return (
<div className="form-horizontal">
<Formik
initialValues={edgePropertiesFormInitialValues}
validationSchema={() => validationSchema(isNomadTokenVisible)}
onSubmit={() => {}}
>
{({ values, setFieldValue }) => (
<>
<EdgeScriptSettingsFieldset
isNomadTokenVisible={
isNomadTokenVisible && values.platform === 'nomad'
}
hideIdGetter={edgeInfo.id !== undefined}
/>
<div className="mt-8">
{showOsSelector && (
<OsSelector
value={values.os}
onChange={(value) => setFieldValue('os', value)}
/>
)}
<ScriptTabs
edgeId={edgeInfo.id}
edgeKey={edgeInfo.key}
values={values}
commands={showOsSelector ? commands[values.os] || [] : commands}
platform={values.platform}
onPlatformChange={(platform) =>
setFieldValue('platform', platform)
}
/>
</div>
</>
)}
</Formik>
</div>
);
}

View file

@ -0,0 +1,11 @@
import { object, boolean, string } from 'yup';
import { validation as nomadTokenValidation } from './NomadTokenField';
export function validationSchema(isNomadTokenVisible?: boolean) {
return object().shape({
allowSelfSignedCertificates: boolean(),
envVars: string(),
...(isNomadTokenVisible ? nomadTokenValidation() : {}),
});
}

View file

@ -0,0 +1,79 @@
import { useFormikContext, Field } from 'formik';
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { Input } from '@/portainer/components/form-components/Input';
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
import { TextTip } from '@/portainer/components/Tip/TextTip';
import { NomadTokenField } from './NomadTokenField';
import { ScriptFormValues } from './types';
interface Props {
isNomadTokenVisible?: boolean;
hideIdGetter?: boolean;
}
export function EdgeScriptSettingsFieldset({
isNomadTokenVisible,
hideIdGetter,
}: Props) {
const { values, setFieldValue } = useFormikContext<ScriptFormValues>();
return (
<>
{!hideIdGetter && (
<>
<FormControl
label="Edge ID Generator"
tooltip="A bash script one liner that will generate the edge id and will be assigned to the PORTAINER_EDGE_ID environment variable"
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">
<TextTip color="blue">
<code>PORTAINER_EDGE_ID</code> environment variable is required
to successfully connect the edge agent to Portainer
</TextTip>
</div>
</div>
</>
)}
{isNomadTokenVisible && <NomadTokenField />}
<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-variables-input"
>
<Field
name="envVars"
as={Input}
placeholder="foo=bar,myvar"
id="env-variables-input"
/>
</FormControl>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
checked={values.allowSelfSignedCertificates}
onChange={(value) =>
setFieldValue('allowSelfSignedCertificates', value)
}
label="Allow self-signed certs"
tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS"
/>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,53 @@
import { Field, useFormikContext } from 'formik';
import { string, boolean } from 'yup';
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
import { Input } from '@/portainer/components/form-components/Input';
import { ScriptFormValues } from './types';
export function NomadTokenField() {
const { values, setFieldValue, errors } =
useFormikContext<ScriptFormValues>();
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
checked={values.authEnabled}
onChange={(value) => {
if (!value) {
setFieldValue('nomadToken', '');
}
setFieldValue('authEnabled', value);
}}
label="Nomad Authentication Enabled"
tooltip="Nomad authentication is only required if you have ACL enabled"
/>
</div>
</div>
{values.authEnabled && (
<FormControl
label="Nomad Token"
inputId="nomad-token-input"
errors={errors.nomadToken}
>
<Field name="nomadToken" as={Input} id="nomad-token-input" />
</FormControl>
)}
</>
);
}
export function validation() {
return {
nomadToken: string().when('authEnabled', {
is: true,
then: string().required('Token is required'),
}),
authEnabled: boolean(),
};
}

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,68 @@
import { useEffect } from 'react';
import { Code } from '@/portainer/components/Code';
import { CopyButton } from '@/portainer/components/Button/CopyButton';
import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails';
import { ScriptFormValues, Platform } from './types';
import { CommandTab } from './scripts';
interface Props {
values: ScriptFormValues;
edgeKey: string;
edgeId?: string;
commands: CommandTab[];
platform?: Platform;
onPlatformChange?(platform: Platform): void;
}
export function ScriptTabs({
values,
edgeKey,
edgeId,
commands,
platform,
onPlatformChange = () => {},
}: Props) {
const agentDetails = useAgentDetails();
useEffect(() => {
if (commands.length > 0 && commands.every((p) => p.id !== platform)) {
onPlatformChange(commands[0].id);
}
}, [platform, onPlatformChange, commands]);
if (!agentDetails) {
return null;
}
const { agentSecret, agentVersion } = agentDetails;
const options = commands.map((c) => {
const cmd = c.command(agentVersion, edgeKey, values, 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>
);
}

View file

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

View file

@ -0,0 +1,240 @@
import _ from 'lodash';
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
import { ScriptFormValues, Platform } from './types';
type CommandGenerator = (
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) => string;
export type CommandTab = {
id: Platform;
label: string;
command: CommandGenerator;
};
export const commandsTabs: Record<string, CommandTab> = {
k8sLinux: {
id: 'k8s',
label: 'Kubernetes',
command: buildLinuxKubernetesCommand,
},
swarmLinux: {
id: 'swarm',
label: 'Docker Swarm',
command: buildLinuxSwarmCommand,
},
standaloneLinux: {
id: 'standalone',
label: 'Docker Standalone',
command: buildLinuxStandaloneCommand,
},
swarmWindows: {
id: 'swarm',
label: 'Docker Swarm',
command: buildWindowsSwarmCommand,
},
standaloneWindow: {
id: 'standalone',
label: 'Docker Standalone',
command: buildWindowsStandaloneCommand,
},
} as const;
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 ');
}
export function buildLinuxStandaloneCommand(
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(
envVars,
buildDefaultEnvVars(
edgeKey,
allowSelfSignedCertificates,
!edgeIdGenerator ? edgeId : undefined,
agentSecret
)
);
return `${
edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \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}
`;
}
export function buildWindowsStandaloneCommand(
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(
envVars,
buildDefaultEnvVars(
edgeKey,
allowSelfSignedCertificates,
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret
)
);
return `${
edgeIdGenerator
? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \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}
`;
}
export function buildLinuxSwarmCommand(
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars(
edgeKey,
allowSelfSignedCertificates,
!edgeIdGenerator ? edgeId : undefined,
agentSecret
),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
]);
return `${
edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \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}
`;
}
export function buildWindowsSwarmCommand(
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const env = buildDockerEnvVars(envVars, [
...buildDefaultEnvVars(
edgeKey,
allowSelfSignedCertificates,
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
agentSecret
),
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
]);
return `${
edgeIdGenerator
? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdGenerator})" \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}
`;
}
export function buildLinuxKubernetesCommand(
agentVersion: string,
edgeKey: string,
properties: ScriptFormValues,
edgeId?: string,
agentSecret?: string
) {
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
const agentShortVersion = getAgentShortVersion(agentVersion);
const envVarsTrimmed = envVars.trim();
const idEnvVar = edgeIdGenerator
? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n`
: '';
const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
const selfSigned = allowSelfSignedCertificates ? '1' : '0';
return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${envVarsTrimmed}"`;
}
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,20 @@
export type Platform = 'standalone' | 'swarm' | 'k8s' | 'nomad';
export type OS = 'win' | 'linux';
export interface ScriptFormValues {
nomadToken: string;
authEnabled: boolean;
allowSelfSignedCertificates: boolean;
envVars: string;
os: OS;
platform: Platform;
edgeIdGenerator?: string;
}
export interface EdgeInfo {
id?: string;
key: string;
}