fix(edge-stack): sync CE code with EE EE-6163 (#10437)

pull/10459/head
cmeng 1 year ago committed by GitHub
parent a0dbabcc5f
commit 66ca73f98b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,13 +3,18 @@ import { useRouter } from '@uirouter/react';
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import {
parseAutoUpdateResponse,
transformAutoUpdateViewModel,
} from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { InfoPanel } from '@/react/portainer/gitops/InfoPanel';
import { RefField } from '@/react/portainer/gitops/RefField';
import { AutoUpdateModel, GitAuthModel } from '@/react/portainer/gitops/types';
import {
AutoUpdateModel,
GitAuthModel,
RelativePathModel,
} from '@/react/portainer/gitops/types';
import {
baseEdgeStackWebhookUrl,
createWebhookId,
@ -28,6 +33,8 @@ import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { Registry } from '@/react/portainer/registries/types';
import { useRegistries } from '@/react/portainer/registries/queries/useRegistries';
import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils';
import { LoadingButton } from '@@/buttons';
import { FormSection } from '@@/form-components/FormSection';
@ -53,6 +60,7 @@ interface FormValues {
authentication: GitAuthModel;
envVars: EnvVar[];
privateRegistryId?: Registry['Id'];
relativePath: RelativePathModel;
}
export function GitForm({ stack }: { stack: EdgeStack }) {
@ -73,6 +81,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) {
autoUpdate: parseAutoUpdateResponse(stack.AutoUpdate),
refName: stack.GitConfig.ReferenceName,
authentication: parseAuthResponse(stack.GitConfig.Authentication),
relativePath: parseRelativePathResponse(stack),
envVars: stack.EnvVars || [],
};
@ -270,6 +279,8 @@ function InnerForm({
errors={errors.authentication}
/>
{isBE && <RelativePathFieldset value={values.relativePath} readonly />}
<EnvironmentVariablesPanel
onChange={(value) => setFieldValue('envVars', value)}
values={values.envVars}

@ -83,6 +83,12 @@ export type EdgeStack = {
Webhook?: string;
StackFileVersion?: number;
EnvVars?: EnvVar[];
SupportRelativePath: boolean;
FilesystemPath?: string;
SupportPerDeviceConfigs?: boolean;
PerDeviceConfigsPath?: string;
PerDeviceConfigsMatchType?: string;
PerDeviceConfigsGroupMatchType?: string;
};
export enum EditorType {

@ -1,3 +1,4 @@
import { ChangeEvent } from 'react';
import {
Combobox,
ComboboxInput,
@ -6,7 +7,6 @@ import {
ComboboxPopover,
} from '@reach/combobox';
import '@reach/combobox/styles.css';
import { ChangeEvent } from 'react';
import clsx from 'clsx';
import { useSearch } from '@/react/portainer/gitops/queries/useSearch';
@ -22,11 +22,15 @@ export function PathSelector({
onChange,
placeholder,
model,
dirOnly,
readOnly,
}: {
value: string;
onChange(value: string): void;
placeholder: string;
model: GitFormModel;
dirOnly?: boolean;
readOnly?: boolean;
}) {
const [searchTerm, setSearchTerm] = useDebounce(value, onChange);
@ -36,6 +40,7 @@ export function PathSelector({
keyword: searchTerm,
reference: model.RepositoryReferenceName,
tlsSkipVerify: model.TLSSkipVerify,
dirOnly,
...creds,
};
const enabled = Boolean(
@ -51,10 +56,11 @@ export function PathSelector({
data-cy="component-gitComposeInput"
>
<ComboboxInput
value={searchTerm}
className="form-control"
onChange={handleChange}
placeholder={placeholder}
value={searchTerm}
readOnly={readOnly}
/>
{searchResults && searchResults.length > 0 && (
<ComboboxPopover>

@ -0,0 +1,251 @@
import { useCallback } from 'react';
import {
GitFormModel,
RelativePathModel,
} from '@/react/portainer/gitops/types';
import { PathSelector } from '@/react/portainer/gitops/ComposePathField/PathSelector';
import { dummyGitForm } from '@/react/portainer/gitops/RelativePathFieldset/utils';
import { useValidation } from '@/react/portainer/gitops/RelativePathFieldset/useValidation';
import { SwitchField } from '@@/form-components/SwitchField';
import { TextTip } from '@@/Tip/TextTip';
import { FormControl } from '@@/form-components/FormControl';
import { Input, Select } from '@@/form-components/Input';
interface Props {
value: RelativePathModel;
gitModel?: GitFormModel;
onChange?: (value: Partial<RelativePathModel>) => void;
readonly?: boolean;
}
export function RelativePathFieldset({
value,
gitModel,
onChange,
readonly,
}: Props) {
const innerOnChange = useCallback(
(value: Partial<RelativePathModel>) => onChange && onChange(value),
[onChange]
);
const { errors } = useValidation(value);
return (
<>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
&apos;Gitops Edge Configuration&apos; requires relative path volumes
to be enabled first, as it uses this feature as the base mechanism.
</TextTip>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
name="EnableRelativePaths"
label="Enable relative path volumes"
labelClass="col-sm-3 col-lg-2"
tooltip="Enabling this means you can specify relative path volumes in your Compose files, with Portainer pulling the content from your git repository to the environment the stack is deployed to."
disabled={readonly}
checked={value.SupportRelativePath}
onChange={(value) => innerOnChange({ SupportRelativePath: value })}
/>
</div>
</div>
{value.SupportRelativePath && (
<>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
For relative path volumes use with Docker Swarm, you must have a
network filesystem which all of your nodes can access.
</TextTip>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<FormControl
label="Local filesystem path"
errors={errors.FilesystemPath}
>
<Input
name="FilesystemPath"
placeholder="/mnt"
disabled={readonly}
value={value.FilesystemPath}
onChange={(e) =>
innerOnChange({ FilesystemPath: e.target.value })
}
/>
</FormControl>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
When enabled, corresponding Edge ID will be passed through as an
environment variable: PORTAINER_EDGE_ID.
</TextTip>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
name="EnablePerDeviceConfigs"
label="GitOps Edge configurations"
labelClass="col-sm-3 col-lg-2"
tooltip="By enabling the GitOps Edge Configurations feature, you gain the ability to define relative path volumes in your configuration files. Portainer will then automatically fetch the content from your git repository by matching the folder name or file name with the Portainer Edge ID, and apply it to the environment where the stack is deployed"
disabled={readonly}
checked={!!value.SupportPerDeviceConfigs}
onChange={(value) =>
innerOnChange({ SupportPerDeviceConfigs: value })
}
/>
</div>
</div>
{value.SupportPerDeviceConfigs && (
<>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
Specify the directory name where your configuration will be
located. This will allow you to manage device configuration
settings with a Git repo as your template.
</TextTip>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<FormControl
label="Directory"
errors={errors.PerDeviceConfigsPath}
>
<PathSelector
value={value.PerDeviceConfigsPath || ''}
onChange={(value) =>
innerOnChange({ PerDeviceConfigsPath: value })
}
placeholder="config"
model={gitModel || dummyGitForm}
readOnly={readonly}
dirOnly
/>
</FormControl>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
Select which rule to use when matching configuration with
Portainer Edge ID either on a per-device basis or group-wide
with an Edge Group. Only configurations that match the
selected rule will be accessible through their corresponding
paths. Deployments that rely on accessing the configuration
may experience errors.
</TextTip>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<FormControl label="Device matching rule">
<Select
value={value.PerDeviceConfigsMatchType}
onChange={(e) =>
innerOnChange({
PerDeviceConfigsMatchType: e.target.value,
})
}
options={[
{
label: '',
value: '',
},
{
label: 'Match file name with Portainer Edge ID',
value: 'file',
},
{
label: 'Match folder name with Portainer Edge ID',
value: 'dir',
},
]}
disabled={readonly}
/>
</FormControl>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<FormControl label="Group matching rule">
<Select
value={value.PerDeviceConfigsGroupMatchType}
onChange={(e) =>
innerOnChange({
PerDeviceConfigsGroupMatchType: e.target.value,
})
}
options={[
{
label: '',
value: '',
},
{
label: 'Match file name with Edge Group',
value: 'file',
},
{
label: 'Match folder name with Edge Group',
value: 'dir',
},
]}
disabled={readonly}
/>
</FormControl>
</div>
</div>
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
<div>
You can use it as an environment variable with an image:{' '}
<code>myapp:$&#123;PORTAINER_EDGE_ID&#125;</code> or{' '}
<code>myapp:$&#123;PORTAINER_EDGE_GROUP&#125;</code>. You
can also use it with the relative path for volumes:{' '}
<code>
./config/$&#123;PORTAINER_EDGE_ID&#125;:/myapp/config
</code>{' '}
or{' '}
<code>
./config/$&#123;PORTAINER_EDGE_GROUP&#125;:/myapp/groupconfig
</code>
. More documentation can be found{' '}
<a href="https://docs.portainer.io/user/edge/stacks/add#gitops-edge-configurations">
here
</a>
.
</div>
</TextTip>
</div>
</div>
</>
)}
</>
)}
</>
);
}

@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
import { FormikErrors, yupToFormErrors } from 'formik';
import { RelativePathModel } from '@/react/portainer/gitops/types';
import { relativePathValidation } from '@/react/portainer/gitops/RelativePathFieldset/validation';
export function useValidation(value: RelativePathModel) {
const [errors, setErrors] = useState<FormikErrors<RelativePathModel>>({});
useEffect(() => {
async function valide() {
try {
await relativePathValidation().validate(value, {
strict: true,
abortEarly: false,
});
setErrors({});
} catch (error) {
setErrors(yupToFormErrors(error));
}
}
valide();
}, [value]);
return { errors };
}

@ -0,0 +1,28 @@
import { EdgeStack } from '@/react/edge/edge-stacks/types';
import { GitFormModel, RelativePathModel } from '../types';
export function parseRelativePathResponse(stack: EdgeStack): RelativePathModel {
return {
SupportRelativePath: stack.SupportRelativePath,
FilesystemPath: stack.FilesystemPath,
SupportPerDeviceConfigs: stack.SupportPerDeviceConfigs,
PerDeviceConfigsMatchType: stack.PerDeviceConfigsMatchType,
PerDeviceConfigsGroupMatchType: stack.PerDeviceConfigsGroupMatchType,
PerDeviceConfigsPath: stack.PerDeviceConfigsPath,
};
}
export const dummyGitForm: GitFormModel = {
RepositoryURL: '',
RepositoryURLValid: false,
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
AdditionalFiles: [],
RepositoryReferenceName: '',
ComposeFilePathInRepository: '',
NewCredentialName: '',
SaveCredential: false,
TLSSkipVerify: false,
};

@ -0,0 +1,24 @@
import { boolean, object, SchemaOf, string } from 'yup';
import { RelativePathModel } from '@/react/portainer/gitops/types';
export function relativePathValidation(): SchemaOf<RelativePathModel> {
return object({
SupportRelativePath: boolean().default(false),
FilesystemPath: string()
.when(['SupportRelativePath'], {
is: true,
then: string().required('Local filesystem path is required'),
})
.default(''),
SupportPerDeviceConfigs: boolean().default(false),
PerDeviceConfigsPath: string()
.when(['SupportPerDeviceConfigs'], {
is: true,
then: string().required('Directory is required'),
})
.default(''),
PerDeviceConfigsMatchType: string().oneOf(['', 'file', 'dir']),
PerDeviceConfigsGroupMatchType: string().oneOf(['', 'file', 'dir']),
});
}

@ -0,0 +1,32 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { PathSelector } from '@/react/portainer/gitops/ComposePathField/PathSelector';
import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
export const ngModule = angular
.module('portainer.app.react.gitops', [])
.component(
'pathSelector',
r2a(withUIRouter(withReactQuery(PathSelector)), [
'value',
'onChange',
'placeholder',
'model',
'dirOnly',
'readOnly',
])
)
.component(
'relativePathFieldset',
r2a(withUIRouter(withReactQuery(RelativePathFieldset)), [
'value',
'gitModel',
'onChange',
'readonly',
])
);
export const gitopsModule = ngModule.name;

@ -69,3 +69,12 @@ export interface GitFormModel extends GitAuthModel {
*/
AutoUpdate?: AutoUpdateModel;
}
export interface RelativePathModel {
SupportRelativePath: boolean;
FilesystemPath?: string;
SupportPerDeviceConfigs?: boolean;
PerDeviceConfigsPath?: string;
PerDeviceConfigsMatchType?: string;
PerDeviceConfigsGroupMatchType?: string;
}

Loading…
Cancel
Save