1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 15:55:23 +02:00

refactor(app): redeploy git form [EE-6444]

This commit is contained in:
testa113 2023-12-14 09:58:10 +13:00
parent 9fc7187e24
commit 2cf7ef90bf
19 changed files with 670 additions and 329 deletions

View file

@ -17,6 +17,7 @@ import {
ApplicationDetailsWidget,
ApplicationEventsDatatable,
} from '@/react/kubernetes/applications/DetailsView';
import { RedeployGitAppForm } from '@/react/kubernetes/applications/components/RedeployGitAppForm';
import { ApplicationContainersDatatable } from '@/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable';
import {
PlacementFormSection,
@ -187,6 +188,13 @@ export const ngModule = angular
'showSystem',
'setSystemResources',
])
)
.component(
'kubeRedeployGitAppForm',
r2a(withUIRouter(withReactQuery(withCurrentUser(RedeployGitAppForm))), [
'namespaceName',
'stackId',
])
);
export const componentsModule = ngModule.name;

View file

@ -150,7 +150,7 @@
<div ng-if="!ctrl.isExternalApplication()">
<git-form-info-panel
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
class-name="'text-muted'"
class-name="'text-muted text-xs'"
url="ctrl.stack.GitConfig.URL"
config-file-path="ctrl.stack.GitConfig.ConfigFilePath"
additional-files="ctrl.stack.AdditionalFiles"
@ -235,11 +235,11 @@
<!-- #endregion -->
<!-- #region Git repository -->
<kubernetes-redeploy-app-git-form
<kube-redeploy-git-app-form
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
stack="ctrl.stack"
stack-id="ctrl.stack.Id"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kubernetes-redeploy-app-git-form>
></kube-redeploy-git-app-form>
<!-- #endregion -->
<!-- #region web editor -->

View file

@ -1,191 +0,0 @@
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { confirm } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { ModalType } from '@@/modals';
import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
import { confirmEnableTLSVerify } from '@/react/portainer/gitops/utils';
class KubernetesRedeployAppGitFormController {
/* @ngInject */
constructor($async, $state, StackService, Notifications) {
this.$async = $async;
this.$state = $state;
this.StackService = StackService;
this.Notifications = Notifications;
this.state = {
saveGitSettingsInProgress: false,
redeployInProgress: false,
showConfig: false,
isEdit: false,
// isAuthEdit is used to preserve the editing state of the AuthFieldset component.
// Within the stack editing page, users have the option to turn the AuthFieldset on or off
// and save the stack setting. If the user enables the AuthFieldset, it implies that they
// must input new Git authentication, rather than edit existing authentication. Thus,
// a dedicated state tracker is required to differentiate between the editing state of
// AuthFieldset component and the whole stack
// When isAuthEdit is true, PAT field needs to be validated.
isAuthEdit: false,
hasUnsavedChanges: false,
baseWebhookUrl: baseStackWebhookUrl(),
webhookId: createWebhookId(),
};
this.formValues = {
RefName: '',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
// auto update
AutoUpdate: parseAutoUpdateResponse(),
};
this.onChange = this.onChange.bind(this);
this.onChangeRef = this.onChangeRef.bind(this);
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
this.onChangeGitAuth = this.onChangeGitAuth.bind(this);
this.onChangeTLSSkipVerify = this.onChangeTLSSkipVerify.bind(this);
}
onChangeRef(value) {
this.onChange({ RefName: value });
}
async onChange(values) {
return this.$async(async () => {
this.formValues = {
...this.formValues,
...values,
};
this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues);
});
}
onChangeGitAuth(values) {
return this.$async(async () => {
this.onChange(values);
});
}
onChangeTLSSkipVerify(value) {
return this.$async(async () => {
if (this.stack.GitConfig.TLSSkipVerify && !value) {
const confirmed = await confirmEnableTLSVerify();
if (!confirmed) {
return;
}
}
this.onChange({ TLSSkipVerify: value });
});
}
async onChangeAutoUpdate(values) {
return this.$async(async () => {
await this.onChange({
AutoUpdate: {
...this.formValues.AutoUpdate,
...values,
},
});
});
}
buildAnalyticsProperties() {
const metadata = {
'automatic-updates': automaticUpdatesLabel(this.formValues.AutoUpdate.RepositoryAutomaticUpdates, this.formValues.AutoUpdate.RepositoryMechanism),
};
return { metadata };
function automaticUpdatesLabel(repositoryAutomaticUpdates, repositoryMechanism) {
switch (repositoryAutomaticUpdates && repositoryMechanism) {
case RepositoryMechanismTypes.INTERVAL:
return 'polling';
case RepositoryMechanismTypes.WEBHOOK:
return 'webhook';
default:
return 'off';
}
}
}
async pullAndRedeployApplication() {
return this.$async(async () => {
try {
const confirmed = await confirm({
title: 'Are you sure?',
message: 'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
confirmButton: buildConfirmButton('Update', 'warning'),
modalType: ModalType.Warn,
});
if (!confirmed) {
return;
}
this.state.redeployInProgress = true;
await this.StackService.updateKubeGit(this.stack.Id, this.stack.EndpointId, this.namespace, this.formValues);
this.Notifications.success('Success', 'Pulled and redeployed stack successfully');
await this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying application');
} finally {
this.state.redeployInProgress = false;
}
});
}
async saveGitSettings() {
return this.$async(async () => {
try {
this.state.saveGitSettingsInProgress = true;
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, { gitConfig: this.formValues, webhookId: this.state.webhookId });
this.savedFormValues = angular.copy(this.formValues);
this.state.hasUnsavedChanges = false;
if (!(this.stack.GitConfig && this.stack.GitConfig.Authentication)) {
// update the AuthFieldset setting
this.state.isAuthEdit = false;
this.formValues.RepositoryUsername = '';
this.formValues.RepositoryPassword = '';
}
this.Notifications.success('Success', 'Save stack settings successfully');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to save application settings');
} finally {
this.state.saveGitSettingsInProgress = false;
}
});
}
isSubmitButtonDisabled() {
return this.state.saveGitSettingsInProgress || this.state.redeployInProgress;
}
$onInit() {
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
this.formValues.TLSSkipVerify = this.stack.GitConfig.TLSSkipVerify;
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
if (this.stack.AutoUpdate && this.stack.AutoUpdate.Webhook) {
this.state.webhookId = this.stack.AutoUpdate.Webhook;
}
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
this.formValues.RepositoryPassword = this.stack.GitConfig.Authentication.Password;
this.formValues.RepositoryAuthentication = true;
this.state.isEdit = true;
this.state.isAuthEdit = true;
}
this.savedFormValues = angular.copy(this.formValues);
}
}
export default KubernetesRedeployAppGitFormController;

View file

@ -1,93 +0,0 @@
<form name="$ctrl.redeployGitForm">
<div class="col-sm-12 form-section-title"> Redeploy from git repository </div>
<div class="form-group text-muted">
<div class="col-sm-12">
<p> Pull the latest manifest from git and redeploy the application. </p>
</div>
</div>
<git-form-auto-update-fieldset
value="$ctrl.formValues.AutoUpdate"
on-change="($ctrl.onChangeAutoUpdate)"
environment-type="KUBERNETES"
is-force-pull-visible="false"
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
webhook-id="{{ $ctrl.state.webhookId }}"
webhooks-docs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
></git-form-auto-update-fieldset>
<time-window-display></time-window-display>
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
<pr-icon ng-if="$ctrl.state.showConfig" icon="'minus'" class-name="'mr-1'"></pr-icon>
<pr-icon ng-if="!$ctrl.state.showConfig" icon="'plus'" class-name="'mr-1'"></pr-icon>
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
</a>
</p>
</div>
</div>
<git-form-ref-field
ng-if="$ctrl.state.showConfig"
value="$ctrl.formValues.RefName"
on-change="($ctrl.onChangeRef)"
model="$ctrl.formValues"
is-url-valid="true"
></git-form-ref-field>
<git-form-auth-fieldset
ng-if="$ctrl.state.showConfig"
value="$ctrl.formValues"
on-change="($ctrl.onChangeGitAuth)"
is-auth-explanation-visible="true"
is-auth-edit="$ctrl.state.isAuthEdit"
></git-form-auth-fieldset>
<div class="form-group" ng-if="$ctrl.state.showConfig">
<div class="col-sm-12">
<por-switch-field
name="TLSSkipVerify"
checked="$ctrl.formValues.TLSSkipVerify"
tooltip="'Enabling this will allow skipping TLS validation for any self-signed certificate.'"
label-class="'col-sm-3 col-lg-2'"
label="'Skip TLS Verification'"
on-change="($ctrl.onChangeTLSSkipVerify)"
></por-switch-field>
</div>
</div>
<div class="col-sm-12 form-section-title"> Actions </div>
<!-- #Git buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.pullAndRedeployApplication()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges|| !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0"
button-spinner="$ctrl.state.redeployInProgress"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-application-edit-git-pull"
>
<span ng-show="!$ctrl.state.redeployInProgress">
<pr-icon icon="'refresh-cw'" class="!mr-1"></pr-icon>
Pull and update application
</span>
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
</button>
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.saveGitSettings()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges|| !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0"
button-spinner="$ctrl.state.saveGitSettingsInProgress"
analytics-on
analytics-category="kubernetes"
analytics-event="kubernetes-application-edit"
analytics-properties="$ctrl.buildAnalyticsProperties()"
>
<span ng-show="!$ctrl.state.saveGitSettingsInProgress"> Save settings </span>
<span ng-show="$ctrl.state.saveGitSettingsInProgress">In progress...</span>
</button>
</form>

View file

@ -1,13 +0,0 @@
import angular from 'angular';
import controller from './kubernetes-redeploy-app-git-form.controller';
const kubernetesRedeployAppGitForm = {
templateUrl: './kubernetes-redeploy-app-git-form.html',
controller,
bindings: {
stack: '<',
namespace: '<',
},
};
angular.module('portainer.app').component('kubernetesRedeployAppGitForm', kubernetesRedeployAppGitForm);

View file

@ -0,0 +1,22 @@
import { StackId } from '@/react/common/stacks/types';
// actions in the url path in api/http/handler/stacks/handler.go
type StackAction =
| 'git'
| 'git/redeploy'
| 'file'
| 'migrate'
| 'start'
| 'stop'
| 'images_status';
export function buildStackUrl(stackId?: StackId, action?: StackAction) {
let url = 'stacks';
if (stackId) {
url += `/${stackId}`;
}
if (action) {
url += `/${action}`;
}
return url;
}

View file

@ -0,0 +1,6 @@
import { StackId } from '../types';
export const stacksQueryKeys = {
stackFile: (stackId: StackId) => ['stacks', stackId, 'file'],
stacks: ['stacks'],
};

View file

@ -0,0 +1,47 @@
import { useQuery } from 'react-query';
import axios from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
import { Stack, StackFile, StackId } from '../types';
import { stacksQueryKeys } from './query-keys';
import { buildStackUrl } from './buildUrl';
export function useStackQuery(stackId?: StackId) {
return useQuery(
stacksQueryKeys.stackFile(stackId || 0),
() => getStack(stackId),
{
...withError('Unable to retrieve stack'),
enabled: !!stackId,
}
);
}
async function getStack(stackId?: StackId) {
if (!stackId) {
return Promise.resolve(undefined);
}
const { data } = await axios.get<Stack>(buildStackUrl(stackId));
return data;
}
export function useStackFileQuery(stackId?: StackId) {
return useQuery(
stacksQueryKeys.stackFile(stackId || 0),
() => getStackFile(stackId),
{
...withError('Unable to retrieve stack'),
enabled: !!stackId,
}
);
}
async function getStackFile(stackId?: StackId) {
if (!stackId) {
return Promise.resolve(undefined);
}
const { data } = await axios.get<StackFile>(buildStackUrl(stackId, 'file'));
return data;
}

View file

@ -0,0 +1,23 @@
import { useQuery } from 'react-query';
import { withError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Stack } from '@/react/common/stacks/types';
import { buildStackUrl } from './buildUrl';
import { stacksQueryKeys } from './query-keys';
export function useStacksQuery() {
return useQuery(stacksQueryKeys.stacks, () => getStacks(), {
...withError('Failed loading stack'),
});
}
export async function getStacks() {
try {
const { data } = await axios.get<Stack[]>(buildStackUrl());
return data;
} catch (e) {
throw parseAxiosError(e as Error);
}
}

View file

@ -0,0 +1,53 @@
import { useMutation } from 'react-query';
import axios from '@/portainer/services/axios';
import {
withError,
withInvalidate,
queryClient,
} from '@/react-tools/react-query';
import { StackId } from '@/react/common/stacks/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
import { stacksQueryKeys } from './query-keys';
import { buildStackUrl } from './buildUrl';
type UpdateKubeGitStackPayload = {
AutoUpdate: AutoUpdateModel;
RepositoryAuthentication: boolean;
RepositoryGitCredentialID: number;
RepositoryPassword: string;
RepositoryReferenceName: string;
RepositoryUsername: string;
TLSSkipVerify: boolean;
};
// update a stack from a git repository
export function useUpdateKubeGitStackMutation(
stackId: StackId,
environmentId: EnvironmentId
) {
return useMutation(
(stack: UpdateKubeGitStackPayload) =>
updateGitStack({ stack, stackId, environmentId }),
{
...withError('Unable to update stack'),
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
}
);
}
function updateGitStack({
stackId,
stack,
environmentId,
}: {
stackId: StackId;
stack: UpdateKubeGitStackPayload;
environmentId: EnvironmentId;
}) {
return axios.put(buildStackUrl(stackId), stack, {
params: { endpointId: environmentId },
});
}

View file

@ -1,25 +0,0 @@
import { useQuery } from 'react-query';
import axios from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
import { StackFile, StackId } from './types';
const queryKeys = {
stackFile: (stackId?: StackId) => ['stacks', stackId, 'file'],
};
export function useStackFile(stackId?: StackId) {
return useQuery(queryKeys.stackFile(stackId), () => getStackFile(stackId), {
...withError('Unable to retrieve stack'),
enabled: !!stackId,
});
}
async function getStackFile(stackId?: StackId) {
if (!stackId) {
return Promise.resolve(undefined);
}
const { data } = await axios.get<StackFile>(`/stacks/${stackId}/file`);
return data;
}

View file

@ -3,7 +3,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
import { Pod } from 'kubernetes-types/core/v1';
import { Authorized } from '@/react/hooks/useUser';
import { useStackFile } from '@/react/common/stacks/stack.service';
import { useStackFileQuery } from '@/react/common/stacks/queries/useStackQuery';
import { Widget, WidgetBody } from '@@/Widget';
import { Button } from '@@/buttons';
@ -48,7 +48,7 @@ export function ApplicationDetailsWidget() {
);
const externalApp = app && isExternalApplication(app);
const appStackId = Number(app?.metadata?.labels?.[appStackIdLabel]);
const appStackFileQuery = useStackFile(appStackId);
const appStackFileQuery = useStackFileQuery(appStackId);
const { data: appServices } = useApplicationServices(
environmentId,
namespace,

View file

@ -0,0 +1,357 @@
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { useRef, useState } from 'react';
import { RefreshCw } from 'lucide-react';
import { Stack, StackId } from '@/react/common/stacks/types';
import {
parseAutoUpdateResponse,
transformAutoUpdateViewModel,
} from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
import {
AutoUpdateMechanism,
AutoUpdateModel,
} from '@/react/portainer/gitops/types';
import {
baseStackWebhookUrl,
createWebhookId,
} from '@/portainer/helpers/webhookHelper';
import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
import { RefField } from '@/react/portainer/gitops/RefField';
import { parseAuthResponse } from '@/react/portainer/gitops/AuthFieldset/utils';
import { confirmEnableTLSVerify } from '@/react/portainer/gitops/utils';
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useAnalytics } from '@/react/hooks/useAnalytics';
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
import { useCreateGitCredentialMutation } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { notifySuccess } from '@/portainer/services/notifications';
import { useStackQuery } from '@/react/common/stacks/queries/useStackQuery';
import { FormSection } from '@@/form-components/FormSection';
import { SwitchField } from '@@/form-components/SwitchField';
import { LoadingButton } from '@@/buttons';
import { buildConfirmButton } from '@@/modals/utils';
import { confirm } from '@@/modals/confirm';
import { ModalType } from '@@/modals';
import { InlineLoader } from '@@/InlineLoader';
import { Alert } from '@@/Alert';
import { useUpdateKubeGitStackMutation } from './queries/useUpdateKubeGitStackMutation';
import {
KubeAppGitFormValues,
RedeployGitStackPayload,
UpdateKubeGitStackPayload,
} from './types';
import { redeployGitAppFormValidationSchema } from './redeployGitAppFormValidationSchema';
import { useRedeployKubeGitStackMutation } from './queries/useRedeployKubeGitStackMutation';
type Props = {
stackId: StackId;
namespaceName: string;
};
export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
const { data: stack, ...stackQuery } = useStackQuery(stackId);
const { trackEvent } = useAnalytics();
const initialValues = getInitialValues(stack);
const { user } = useCurrentUser();
const { data: gitCredentials, ...gitCredentialsQuery } = useGitCredentials(
user.Id
);
const environmentId = useEnvironmentId();
const createGitCredentialMutation = useCreateGitCredentialMutation();
const updateKubeGitStackMutation = useUpdateKubeGitStackMutation(
stack?.Id || 0,
environmentId
);
// keep a single generated webhook id, so that it doesn't change on every render
const generatedWebhookId = useRef(createWebhookId());
const webhookId = stack?.AutoUpdate?.Webhook || generatedWebhookId.current;
if (!initialValues) {
return null;
}
const isAuthEdit = !!initialValues.authentication.RepositoryUsername;
if (stackQuery.isLoading || gitCredentialsQuery.isLoading) {
return (
<InlineLoader>Loading application git configuration...</InlineLoader>
);
}
if (stackQuery.isError) {
return (
<Alert color="error">Unable to load application git configuration.</Alert>
);
}
if (!stack) {
return null;
}
return (
<FormSection titleSize="lg" title="Redeploy from git repository">
<div className="form-group text-muted">
<div className="col-sm-12">
<p>Pull the latest manifest from git and redeploy the application.</p>
</div>
</div>
<Formik<KubeAppGitFormValues>
initialValues={initialValues}
validationSchema={() =>
redeployGitAppFormValidationSchema(gitCredentials || [], isAuthEdit)
}
onSubmit={handleSubmit}
>
<RedeployGitAppInnerForm
stack={stack}
namespaceName={namespaceName}
webhookId={webhookId}
/>
</Formik>
</FormSection>
);
async function handleSubmit(
values: KubeAppGitFormValues,
{ resetForm }: FormikHelpers<KubeAppGitFormValues>
) {
trackSubmit(values);
// save the new git credentials if the user has selected to save them
if (
values.authentication.SaveCredential &&
values.authentication.NewCredentialName &&
values.authentication.RepositoryPassword &&
values.authentication.RepositoryUsername
) {
createGitCredentialMutation.mutate({
name: values.authentication.NewCredentialName,
username: values.authentication.RepositoryUsername,
password: values.authentication.RepositoryPassword,
userId: user.Id,
});
}
// update the kubernetes git stack
const { authentication } = values;
const updateKubeGitStack: UpdateKubeGitStackPayload = {
RepositoryAuthentication:
!!values.authentication.RepositoryAuthentication,
RepositoryReferenceName: values.repositoryReferenceName,
AutoUpdate: transformAutoUpdateViewModel(values.autoUpdate, webhookId),
TLSSkipVerify: values.tlsSkipVerify,
RepositoryGitCredentialID: authentication.RepositoryGitCredentialID,
RepositoryPassword: authentication.RepositoryPassword,
RepositoryUsername: authentication.RepositoryUsername,
};
await updateKubeGitStackMutation.mutateAsync(updateKubeGitStack, {
onSuccess: ({ data: newStack }) => {
const newInitialValues = getInitialValues(newStack);
notifySuccess('Success', 'Application saved successfully.');
resetForm({ values: newInitialValues });
},
});
}
function trackSubmit(values: KubeAppGitFormValues) {
trackEvent('kubernetes-application-edit', {
category: 'kubernetes',
metadata: {
'automatic-updates': automaticUpdatesLabel(
values.autoUpdate.RepositoryAutomaticUpdates,
values.autoUpdate.RepositoryMechanism
),
},
});
function automaticUpdatesLabel(
repositoryAutomaticUpdates: boolean,
repositoryMechanism: AutoUpdateMechanism
) {
switch (repositoryAutomaticUpdates && repositoryMechanism) {
case RepositoryMechanismTypes.INTERVAL:
return 'polling';
case RepositoryMechanismTypes.WEBHOOK:
return 'webhook';
default:
return 'off';
}
}
}
}
function RedeployGitAppInnerForm({
stack,
namespaceName,
webhookId,
}: {
stack: Stack;
namespaceName: string;
webhookId: string;
}) {
const environmentId = useEnvironmentId();
const redeployKubeGitStackMutation = useRedeployKubeGitStackMutation(
stack.Id,
environmentId
);
const [isRedeployLoading, setIsRedeployLoading] = useState(false);
const { trackEvent } = useAnalytics();
const {
values,
errors,
setFieldValue,
handleSubmit,
dirty,
isValid,
isSubmitting,
setFieldTouched,
} = useFormikContext<KubeAppGitFormValues>();
return (
<Form className="form-horizontal" onSubmit={handleSubmit}>
<AutoUpdateFieldset
value={values.autoUpdate}
onChange={(value: AutoUpdateModel) =>
setFieldValue('autoUpdate', value)
}
baseWebhookUrl={baseStackWebhookUrl()}
webhookId={webhookId}
errors={errors.autoUpdate}
environmentType="KUBERNETES"
isForcePullVisible={false}
webhooksDocs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
/>
<TimeWindowDisplay />
<FormSection title="Advanced configuration" isFoldable>
<RefField
value={values.repositoryReferenceName}
onChange={(refName) =>
setFieldValue('repositoryReferenceName', refName)
}
model={{
...values.authentication,
RepositoryURL: values.repositoryURL,
}}
error={errors.repositoryReferenceName}
stackId={stack.Id}
isUrlValid
/>
<AuthFieldset
value={values.authentication}
isAuthExplanationVisible
onChange={(value) =>
Object.entries(value).forEach(([key, value]) => {
setFieldValue(key, value);
// set touched after a delay to revalidate debounced username and access token fields
setTimeout(() => setFieldTouched(key, true), 400);
})
}
errors={errors.authentication}
/>
<SwitchField
name="TLSSkipVerify"
label="Skip TLS verification"
labelClass="col-sm-3 col-lg-2"
tooltip="Enabling this will allow skipping TLS validation for any self-signed certificate."
checked={values.tlsSkipVerify}
onChange={onChangeTLSSkipVerify}
/>
</FormSection>
<FormSection title="Actions">
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
type="button"
onClick={handleRedeploy}
className="!ml-0"
loadingText="In progress..."
isLoading={isRedeployLoading}
disabled={dirty}
icon={RefreshCw}
data-cy="application-redeploy-button"
>
Pull and update application
</LoadingButton>
<LoadingButton
type="submit"
loadingText="In progress..."
isLoading={isSubmitting}
disabled={!dirty || !isValid}
data-cy="application-redeploy-button"
>
Save settings
</LoadingButton>
</div>
</div>
</FormSection>
</Form>
);
async function handleRedeploy() {
const confirmed = await confirm({
title: 'Are you sure?',
message:
'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
confirmButton: buildConfirmButton('Update', 'warning'),
modalType: ModalType.Warn,
});
if (!confirmed) {
return;
}
setIsRedeployLoading(true);
// track the redeploy event
trackEvent('kubernetes-application-edit-git-pull', {
category: 'kubernetes',
});
// redeploy the application
const redeployPayload: RedeployGitStackPayload = {
RepositoryReferenceName: values.repositoryReferenceName,
RepositoryAuthentication:
!!values.authentication.RepositoryAuthentication,
RepositoryGitCredentialID:
values.authentication.RepositoryGitCredentialID,
RepositoryPassword: values.authentication.RepositoryPassword,
RepositoryUsername: values.authentication.RepositoryUsername,
Namespace: namespaceName,
};
await redeployKubeGitStackMutation.mutateAsync(redeployPayload, {
onSuccess: () => {
notifySuccess('Success', 'Application redeployed successfully.');
},
});
setIsRedeployLoading(false);
}
async function onChangeTLSSkipVerify(value: boolean) {
// If the user is disabling TLS verification, ask for confirmation
if (stack.GitConfig?.TLSSkipVerify && !value) {
const confirmed = await confirmEnableTLSVerify();
if (!confirmed) {
return;
}
}
setFieldValue('tlsSkipVerify', value);
}
}
function getInitialValues(stack?: Stack): KubeAppGitFormValues | undefined {
if (!stack || !stack.GitConfig) {
return undefined;
}
const autoUpdate = parseAutoUpdateResponse(stack.AutoUpdate);
const authentication = parseAuthResponse(stack.GitConfig?.Authentication);
return {
authentication,
repositoryReferenceName: stack.GitConfig.ReferenceName,
repositoryURL: stack.GitConfig.URL,
tlsSkipVerify: stack.GitConfig.TLSSkipVerify,
autoUpdate,
};
}

View file

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

View file

@ -0,0 +1,54 @@
import { useMutation } from 'react-query';
import { buildStackUrl } from '@/react/common/stacks/queries/buildUrl';
import { stacksQueryKeys } from '@/react/common/stacks/queries/query-keys';
import { StackId } from '@/react/common/stacks/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
withError,
withInvalidate,
queryClient,
} from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { RedeployGitStackPayload } from '../types';
export function useRedeployKubeGitStackMutation(
stackId: StackId,
environmentId: EnvironmentId
) {
return useMutation(
(payload: RedeployGitStackPayload) =>
redeployKubeGitStack({
stackId,
redeployPayload: payload,
environmentId,
}),
{
...withError('Unable to redeploy application.'),
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
}
);
}
async function redeployKubeGitStack({
stackId,
redeployPayload,
environmentId,
}: {
stackId: StackId;
environmentId: EnvironmentId;
redeployPayload: RedeployGitStackPayload;
}) {
try {
return await axios.put(
buildStackUrl(stackId, 'git/redeploy'),
redeployPayload,
{
params: { endpointId: environmentId },
}
);
} catch (e) {
throw parseAxiosError(e);
}
}

View file

@ -0,0 +1,47 @@
import { useMutation } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import {
withError,
withInvalidate,
queryClient,
} from '@/react-tools/react-query';
import { Stack, StackId } from '@/react/common/stacks/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { stacksQueryKeys } from '@/react/common/stacks/queries/query-keys';
import { buildStackUrl } from '@/react/common/stacks/queries/buildUrl';
import { UpdateKubeGitStackPayload } from '../types';
// update a stack from a git repository
export function useUpdateKubeGitStackMutation(
stackId: StackId,
environmentId: EnvironmentId
) {
return useMutation(
(stack: UpdateKubeGitStackPayload) =>
updateGitStack({ stack, stackId, environmentId }),
{
...withError('Unable to update application.'),
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
}
);
}
async function updateGitStack({
stackId,
stack,
environmentId,
}: {
stackId: StackId;
stack: UpdateKubeGitStackPayload;
environmentId: EnvironmentId;
}) {
try {
return await axios.post<Stack>(buildStackUrl(stackId, 'git'), stack, {
params: { endpointId: environmentId },
});
} catch (e) {
throw parseAxiosError(e);
}
}

View file

@ -0,0 +1,20 @@
import { SchemaOf, boolean, object, string } from 'yup';
import { autoUpdateValidation } from '@/react/portainer/gitops/AutoUpdateFieldset/validation';
import { gitAuthValidation } from '@/react/portainer/gitops/AuthFieldset';
import { GitCredential } from '@/react/portainer/account/git-credentials/types';
import { KubeAppGitFormValues } from './types';
export function redeployGitAppFormValidationSchema(
gitCredentials: Array<GitCredential>,
isAuthEdit: boolean
): SchemaOf<KubeAppGitFormValues> {
return object({
authentication: gitAuthValidation(gitCredentials, isAuthEdit),
repositoryURL: string().required('Repository URL is required'),
tlsSkipVerify: boolean().required(),
repositoryReferenceName: string().required('Branch is required'),
autoUpdate: autoUpdateValidation(),
});
}

View file

@ -0,0 +1,25 @@
import {
AutoUpdateModel,
AutoUpdateResponse,
GitAuthModel,
GitCredentialsModel,
} from '@/react/portainer/gitops/types';
export interface KubeAppGitFormValues {
authentication: GitAuthModel;
repositoryReferenceName: string;
repositoryURL: string;
tlsSkipVerify: boolean;
autoUpdate: AutoUpdateModel;
}
export interface UpdateKubeGitStackPayload extends GitCredentialsModel {
AutoUpdate: AutoUpdateResponse | null;
RepositoryReferenceName: string;
TLSSkipVerify: boolean;
}
export interface RedeployGitStackPayload extends GitCredentialsModel {
RepositoryReferenceName: string;
Namespace: string;
}

View file

@ -58,7 +58,7 @@ export async function checkRepo(
if (
(!(creds.username && creds.password) || !creds.gitCredentialId) &&
details ===
'authentication failed, please ensure that the git credentials are correct'
'Authentication failed, please ensure that the git credentials are correct'
) {
details =
'Git repository could not be found or is private, please ensure that the URL is correct or credentials are provided.';