mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:20:11 +00:00
feat(environments): offer edge connectivity test before adding edge environments [c9s-149] (#2527)
This commit is contained in:
@@ -11,6 +11,7 @@ export interface Props {
|
||||
color?: Color;
|
||||
className?: string;
|
||||
childrenWrapperClassName?: string;
|
||||
/** Whether the tip should be displayed inline or as a block. Defaults to inline. */
|
||||
inline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails';
|
||||
|
||||
import { Code } from '@@/Code';
|
||||
import { CopyButton } from '@@/buttons/CopyButton';
|
||||
import { Modal } from '@@/modals/Modal';
|
||||
import { Button } from '@@/buttons';
|
||||
import { NavTabs } from '@@/NavTabs';
|
||||
import { NavContainer } from '@@/NavTabs/NavContainer';
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
|
||||
export type ConnectivityEnvironment = 'docker' | 'podman' | 'kubernetes';
|
||||
|
||||
const ALL_ENVIRONMENTS: ConnectivityEnvironment[] = [
|
||||
'docker',
|
||||
'podman',
|
||||
'kubernetes',
|
||||
];
|
||||
|
||||
const ENVIRONMENT_LABELS: Record<ConnectivityEnvironment, string> = {
|
||||
docker: 'Docker',
|
||||
podman: 'Podman',
|
||||
kubernetes: 'Kubernetes',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onDismiss: () => void;
|
||||
portainerUrl: string;
|
||||
tunnelServerAddr?: string;
|
||||
/** When provided, show only this environment. When omitted, show tabs for all environments. */
|
||||
environment?: ConnectivityEnvironment;
|
||||
}
|
||||
|
||||
export function ConnectivityTestModal({
|
||||
onDismiss,
|
||||
portainerUrl,
|
||||
tunnelServerAddr,
|
||||
environment,
|
||||
}: Props) {
|
||||
const environments = environment ? [environment] : ALL_ENVIRONMENTS;
|
||||
const [selectedTab, setSelectedTab] = useState<ConnectivityEnvironment>(
|
||||
environments[0]
|
||||
);
|
||||
const [insecurePoll, setInsecurePoll] = useState(true);
|
||||
const agentDetails = useAgentDetails();
|
||||
const agentVersion = agentDetails?.agentVersion ?? 'latest';
|
||||
|
||||
const options = environments.map((env) => {
|
||||
const command = buildCommand(
|
||||
env,
|
||||
portainerUrl,
|
||||
tunnelServerAddr,
|
||||
insecurePoll,
|
||||
agentVersion
|
||||
);
|
||||
const label = ENVIRONMENT_LABELS[env];
|
||||
return {
|
||||
id: env,
|
||||
label,
|
||||
children: (
|
||||
<>
|
||||
<Code>{command}</Code>
|
||||
<div className="mt-2">
|
||||
<CopyButton
|
||||
copyText={command}
|
||||
data-cy="copy-connectivity-test-command-button"
|
||||
>
|
||||
Copy command
|
||||
</CopyButton>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal onDismiss={onDismiss} aria-label="Test connectivity" size="lg">
|
||||
<Modal.Header title="Test connectivity" />
|
||||
<Modal.Body>
|
||||
<p className="mb-4">
|
||||
Run the command in the environment where the Edge Agent will be
|
||||
deployed to verify it can reach the Portainer server.
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<SwitchField
|
||||
checked={insecurePoll}
|
||||
onChange={setInsecurePoll}
|
||||
label="Allow self-signed certificates"
|
||||
labelClass="col-sm-4 col-lg-3"
|
||||
tooltip="Include EDGE_INSECURE_POLL=1 in the script. Enable this if your Portainer instance uses a self-signed or untrusted certificate."
|
||||
data-cy="connectivity-insecure-poll-switch"
|
||||
/>
|
||||
</div>
|
||||
<NavContainer>
|
||||
<NavTabs
|
||||
selectedId={selectedTab}
|
||||
options={options}
|
||||
onSelect={(id: ConnectivityEnvironment) => setSelectedTab(id)}
|
||||
/>
|
||||
</NavContainer>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
onClick={onDismiss}
|
||||
color="default"
|
||||
data-cy="close-connectivity-test-modal-button"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function buildCommand(
|
||||
environment: ConnectivityEnvironment,
|
||||
portainerUrl: string,
|
||||
tunnelServerAddr?: string,
|
||||
allowInsecurePoll?: boolean,
|
||||
agentVersion = 'latest'
|
||||
): string {
|
||||
const envVars = [
|
||||
'EDGE_CONNECTIVITY_CHECK=1',
|
||||
`EDGE_CONNECTIVITY_CHECK_URL=${portainerUrl}`,
|
||||
`EDGE_INSECURE_POLL=${allowInsecurePoll ? '1' : '0'}`,
|
||||
...(tunnelServerAddr
|
||||
? [`EDGE_CONNECTIVITY_CHECK_TUNNEL_ADDR=${tunnelServerAddr}`]
|
||||
: []),
|
||||
];
|
||||
|
||||
const image = `portainer/agent:${agentVersion}`;
|
||||
|
||||
switch (environment) {
|
||||
case 'kubernetes':
|
||||
return [
|
||||
`kubectl run portainer-connectivity-check \\`,
|
||||
` --rm --attach --restart=Never \\`,
|
||||
` --image=${image} \\`,
|
||||
...envVars.map((v, i) =>
|
||||
i < envVars.length - 1 ? ` --env="${v}" \\` : ` --env="${v}"`
|
||||
),
|
||||
].join('\n');
|
||||
case 'podman':
|
||||
return [
|
||||
`sudo podman run --rm \\`,
|
||||
...envVars.map((v) => ` -e ${v} \\`),
|
||||
` docker.io/${image}`,
|
||||
].join('\n');
|
||||
case 'docker':
|
||||
default:
|
||||
return [
|
||||
`docker run --rm \\`,
|
||||
...envVars.map((v) => ` -e ${v} \\`),
|
||||
` ${image}`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Laptop } from 'lucide-react';
|
||||
import { Laptop, Network } from 'lucide-react';
|
||||
|
||||
import { generateKey } from '@/react/portainer/environments/environment.service/edge';
|
||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
@@ -8,12 +8,14 @@ import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||
import { useSettings } from '@/react/portainer/settings/queries';
|
||||
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
|
||||
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
|
||||
import { ConnectivityTestModal } from '@/react/edge/components/ConnectivityTestModal/ConnectivityTestModal';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { BoxSelector } from '@@/BoxSelector';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { CopyButton } from '@@/buttons';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
@@ -140,6 +142,8 @@ function EdgeKeyInfo({
|
||||
tunnelUrl?: string;
|
||||
asyncMode: boolean;
|
||||
}) {
|
||||
const [isConnectivityModalOpen, setIsConnectivityModalOpen] = useState(false);
|
||||
|
||||
if (isLoading || !edgeKey) {
|
||||
return <div>Generating key for {url} ... </div>;
|
||||
}
|
||||
@@ -192,7 +196,25 @@ function EdgeKeyInfo({
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color="default"
|
||||
className="!ml-0 mb-8"
|
||||
icon={Network}
|
||||
onClick={() => setIsConnectivityModalOpen(true)}
|
||||
data-cy="edge-auto-create-test-connectivity-button"
|
||||
>
|
||||
Test connectivity
|
||||
</Button>
|
||||
</EdgeScriptForm>
|
||||
|
||||
{isConnectivityModalOpen && url && (
|
||||
<ConnectivityTestModal
|
||||
portainerUrl={url}
|
||||
tunnelServerAddr={!asyncMode ? tunnelUrl : undefined}
|
||||
onDismiss={() => setIsConnectivityModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
|
||||
titleSize="sm"
|
||||
isFoldable
|
||||
defaultFolded={false}
|
||||
className="[&>label]:mb-5"
|
||||
className="mb-8"
|
||||
>
|
||||
<p className="text-muted mb-2 text-xs">
|
||||
These are legacy options that don't support edge features or
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ export function WizardKubernetes({ onCreate }: Props) {
|
||||
titleSize="sm"
|
||||
isFoldable
|
||||
defaultFolded={false}
|
||||
className="[&>label]:mb-5"
|
||||
className="mb-8"
|
||||
>
|
||||
<p className="text-muted mb-2 text-xs">
|
||||
These are legacy options that don't support edge features or
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export function WizardPodman({ onCreate }: Props) {
|
||||
titleSize="sm"
|
||||
isFoldable
|
||||
defaultFolded={false}
|
||||
className="[&>label]:mb-5"
|
||||
className="mb-8"
|
||||
>
|
||||
<p className="text-muted mb-2 text-xs">
|
||||
These are legacy options that don't support edge features or
|
||||
|
||||
+65
-2
@@ -1,14 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Network } from 'lucide-react';
|
||||
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField';
|
||||
import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField';
|
||||
import { NameField } from '@/react/portainer/environments/common/NameField/NameField';
|
||||
import { ContainerEngine } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
ConnectivityEnvironment,
|
||||
ConnectivityTestModal,
|
||||
} from '@/react/edge/components/ConnectivityTestModal/ConnectivityTestModal';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { FormValues } from './types';
|
||||
|
||||
interface EdgeAgentFormProps {
|
||||
readonly?: boolean;
|
||||
asyncMode?: boolean;
|
||||
containerEngine?: ContainerEngine;
|
||||
}
|
||||
|
||||
export function EdgeAgentFieldset({ readonly, asyncMode }: EdgeAgentFormProps) {
|
||||
export function EdgeAgentFieldset({
|
||||
readonly,
|
||||
asyncMode,
|
||||
containerEngine = ContainerEngine.Docker,
|
||||
}: EdgeAgentFormProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { values } = useFormikContext<FormValues>();
|
||||
|
||||
const showTunnelField = isBE && !asyncMode;
|
||||
const environment = toConnectivityEnvironment(containerEngine);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NameField readonly={readonly} />
|
||||
@@ -17,13 +41,52 @@ export function EdgeAgentFieldset({ readonly, asyncMode }: EdgeAgentFormProps) {
|
||||
readonly={readonly}
|
||||
required
|
||||
/>
|
||||
{isBE && !asyncMode && (
|
||||
{showTunnelField && (
|
||||
<PortainerTunnelAddrField
|
||||
fieldName="tunnelServerAddr"
|
||||
readonly={readonly}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<Button
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
icon={Network}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
data-cy="edge-agent-test-connectivity-button"
|
||||
>
|
||||
Test connectivity
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<ConnectivityTestModal
|
||||
portainerUrl={values.portainerUrl}
|
||||
tunnelServerAddr={
|
||||
showTunnelField ? values.tunnelServerAddr : undefined
|
||||
}
|
||||
environment={environment}
|
||||
onDismiss={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function toConnectivityEnvironment(
|
||||
containerEngine: ContainerEngine
|
||||
): ConnectivityEnvironment {
|
||||
switch (containerEngine) {
|
||||
case ContainerEngine.Kubernetes:
|
||||
return 'kubernetes';
|
||||
case ContainerEngine.Podman:
|
||||
return 'podman';
|
||||
case ContainerEngine.Docker:
|
||||
default:
|
||||
return 'docker';
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -60,7 +60,11 @@ export function EdgeAgentForm({
|
||||
>
|
||||
{({ isValid, setFieldValue, values }) => (
|
||||
<Form>
|
||||
<EdgeAgentFieldset readonly={readonly} asyncMode={asyncMode} />
|
||||
<EdgeAgentFieldset
|
||||
readonly={readonly}
|
||||
asyncMode={asyncMode}
|
||||
containerEngine={containerEngine}
|
||||
/>
|
||||
|
||||
<MoreSettingsSection>
|
||||
<FormSection title="Check-in Intervals">
|
||||
|
||||
+28
-1
@@ -1,16 +1,19 @@
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Laptop } from 'lucide-react';
|
||||
import { Laptop, Network } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Settings } from '@/react/portainer/settings/types';
|
||||
import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField';
|
||||
import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { ConnectivityTestModal } from '@/react/edge/components/ConnectivityTestModal/ConnectivityTestModal';
|
||||
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { validationSchema } from './EdgeComputeSettings.validation';
|
||||
import { FormValues } from './types';
|
||||
@@ -21,6 +24,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||
const [isConnectivityModalOpen, setIsConnectivityModalOpen] = useState(false);
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,6 +97,28 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||
/>
|
||||
|
||||
<PortainerTunnelAddrField fieldName="Edge.TunnelServerAddress" />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<Button
|
||||
color="default"
|
||||
icon={Network}
|
||||
onClick={() => setIsConnectivityModalOpen(true)}
|
||||
data-cy="edge-compute-test-connectivity-button"
|
||||
className="!ml-0"
|
||||
>
|
||||
Test connectivity
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConnectivityModalOpen && (
|
||||
<ConnectivityTestModal
|
||||
portainerUrl={values.EdgePortainerUrl}
|
||||
tunnelServerAddr={values.Edge.TunnelServerAddress}
|
||||
onDismiss={() => setIsConnectivityModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user