fix(k8s/gitops): missing git auth toggle in k8s app edit page [EE-5320] (#8741)

pull/8755/head
Oscar Zhou 2023-04-10 20:14:13 +12:00 committed by GitHub
parent 5ac1ea3df8
commit a65ffe519a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 93 additions and 29 deletions

View File

@ -151,6 +151,9 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
password := payload.RepositoryPassword password := payload.RepositoryPassword
// When the existing stack is using the custom username/password and the password is not updated,
// the stack should keep using the saved username/password
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil { if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password password = stack.GitConfig.Authentication.Password
} }

View File

@ -139,6 +139,9 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
repositoryPassword := "" repositoryPassword := ""
if payload.RepositoryAuthentication { if payload.RepositoryAuthentication {
repositoryPassword = payload.RepositoryPassword repositoryPassword = payload.RepositoryPassword
// When the existing stack is using the custom username/password and the password is not updated,
// the stack should keep using the saved username/password
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil { if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
repositoryPassword = stack.GitConfig.Authentication.Password repositoryPassword = stack.GitConfig.Authentication.Password
} }

View File

@ -7,6 +7,7 @@ import { GitAuthModel } from '@/react/portainer/gitops/types';
import { gitAuthValidation } from '@/react/portainer/gitops/AuthFieldset'; import { gitAuthValidation } from '@/react/portainer/gitops/AuthFieldset';
import { GitCredential } from '@/react/portainer/account/git-credentials/types'; import { GitCredential } from '@/react/portainer/account/git-credentials/types';
import { getGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; import { getGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { validateForm } from '@@/form-components/validate-form'; import { validateForm } from '@@/form-components/validate-form';
@ -23,6 +24,8 @@ export default class GitFormAuthFieldsetController {
value?: GitAuthModel; value?: GitAuthModel;
isAuthEdit: boolean;
onChange?: (value: GitAuthModel) => void; onChange?: (value: GitAuthModel) => void;
/* @ngInject */ /* @ngInject */
@ -33,6 +36,7 @@ export default class GitFormAuthFieldsetController {
this.$async = $async; this.$async = $async;
this.Authentication = Authentication; this.Authentication = Authentication;
this.isAuthEdit = false;
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.runGitValidation = this.runGitValidation.bind(this); this.runGitValidation = this.runGitValidation.bind(this);
} }
@ -48,10 +52,10 @@ export default class GitFormAuthFieldsetController {
...newValues, ...newValues,
}; };
this.onChange?.(value); this.onChange?.(value);
await this.runGitValidation(value); await this.runGitValidation(value, false);
} }
async runGitValidation(value: GitAuthModel) { async runGitValidation(value: GitAuthModel, isAuthEdit: boolean) {
return this.$async(async () => { return this.$async(async () => {
this.errors = {}; this.errors = {};
this.gitFormAuthFieldset?.$setValidity( this.gitFormAuthFieldset?.$setValidity(
@ -61,7 +65,7 @@ export default class GitFormAuthFieldsetController {
); );
this.errors = await validateForm<GitAuthModel>( this.errors = await validateForm<GitAuthModel>(
() => gitAuthValidation(this.gitCredentials), () => gitAuthValidation(this.gitCredentials, isAuthEdit),
value value
); );
if (this.errors && Object.keys(this.errors).length > 0) { if (this.errors && Object.keys(this.errors).length > 0) {
@ -75,23 +79,24 @@ export default class GitFormAuthFieldsetController {
} }
async $onInit() { async $onInit() {
try { if (isBE) {
this.gitCredentials = await getGitCredentials( try {
this.Authentication.getUserDetails().ID // Only BE version support /gitcredentials
); this.gitCredentials = await getGitCredentials(
} catch (err) { this.Authentication.getUserDetails().ID
notifyError( );
'Failure', } catch (err) {
err as Error, notifyError(
'Unable to retrieve user saved git credentials' 'Failure',
); err as Error,
'Unable to retrieve user saved git credentials'
);
}
} }
// this should never happen, but just in case // this should never happen, but just in case
if (!this.value) { if (!this.value) {
throw new Error('GitFormController: value is required'); throw new Error('GitFormController: value is required');
} }
await this.runGitValidation(this.value, this.isAuthEdit);
await this.runGitValidation(this.value);
} }
} }

View File

@ -9,13 +9,15 @@ export const gitFormAuthFieldset: IComponentOptions = {
<react-git-form-auth-fieldset <react-git-form-auth-fieldset
value="$ctrl.value" value="$ctrl.value"
on-change="$ctrl.handleChange" on-change="$ctrl.handleChange"
is-explanation-visible="$ctrl.isExplanationVisible" is-auth-explanation-visible="$ctrl.isAuthExplanationVisible"
errors="$ctrl.errors"> errors="$ctrl.errors"
is-auth-edit="$ctrl.isAuthEdit">
</react-git-form-auth-fieldset> </react-git-form-auth-fieldset>
</ng-form>`, </ng-form>`,
bindings: { bindings: {
value: '<', value: '<',
onChange: '<', onChange: '<',
isExplanationVisible: '<', isAuthExplanationVisible: '<',
isAuthEdit: '<',
}, },
}; };

View File

@ -18,6 +18,15 @@ class KubernetesRedeployAppGitFormController {
redeployInProgress: false, redeployInProgress: false,
showConfig: false, showConfig: false,
isEdit: 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, hasUnsavedChanges: false,
baseWebhookUrl: baseStackWebhookUrl(), baseWebhookUrl: baseStackWebhookUrl(),
webhookId: createWebhookId(), webhookId: createWebhookId(),
@ -121,6 +130,13 @@ class KubernetesRedeployAppGitFormController {
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, { gitConfig: this.formValues, webhookId: this.state.webhookId }); 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.savedFormValues = angular.copy(this.formValues);
this.state.hasUnsavedChanges = false; 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'); this.Notifications.success('Success', 'Save stack settings successfully');
} catch (err) { } catch (err) {
this.Notifications.error('Failure', err, 'Unable to save application settings'); this.Notifications.error('Failure', err, 'Unable to save application settings');
@ -139,14 +155,17 @@ class KubernetesRedeployAppGitFormController {
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate); this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
if (this.stack.AutoUpdate.Webhook) { if (this.stack.AutoUpdate && this.stack.AutoUpdate.Webhook) {
this.state.webhookId = this.stack.AutoUpdate.Webhook; this.state.webhookId = this.stack.AutoUpdate.Webhook;
} }
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
this.formValues.RepositoryPassword = this.stack.GitConfig.Authentication.Password;
this.formValues.RepositoryAuthentication = true; this.formValues.RepositoryAuthentication = true;
this.state.isEdit = true; this.state.isEdit = true;
this.state.isAuthEdit = true;
} }
this.savedFormValues = angular.copy(this.formValues); this.savedFormValues = angular.copy(this.formValues);

View File

@ -35,6 +35,14 @@
is-url-valid="true" is-url-valid="true"
></git-form-ref-field> ></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="col-sm-12 form-section-title"> Actions </div> <div class="col-sm-12 form-section-title"> Actions </div>
<!-- #Git buttons --> <!-- #Git buttons -->
<button <button

View File

@ -21,6 +21,15 @@ class StackRedeployGitFormController {
redeployInProgress: false, redeployInProgress: false,
showConfig: false, showConfig: false,
isEdit: 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, hasUnsavedChanges: false,
baseWebhookUrl: baseStackWebhookUrl(), baseWebhookUrl: baseStackWebhookUrl(),
webhookId: createWebhookId(), webhookId: createWebhookId(),
@ -140,6 +149,12 @@ class StackRedeployGitFormController {
this.state.hasUnsavedChanges = false; this.state.hasUnsavedChanges = false;
this.Notifications.success('Success', 'Save stack settings successfully'); this.Notifications.success('Success', 'Save stack settings successfully');
if (!(this.stack.GitConfig && this.stack.GitConfig.Authentication)) {
// update the AuthFieldset setting
this.state.isAuthEdit = false;
this.formValues.RepositoryUsername = '';
this.formValues.RepositoryPassword = '';
}
this.stack = stack; this.stack = stack;
} catch (err) { } catch (err) {
this.Notifications.error('Failure', err, 'Unable to save stack settings'); this.Notifications.error('Failure', err, 'Unable to save stack settings');
@ -188,8 +203,10 @@ class StackRedeployGitFormController {
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
this.formValues.RepositoryPassword = this.stack.GitConfig.Authentication.Password;
this.formValues.RepositoryAuthentication = true; this.formValues.RepositoryAuthentication = true;
this.state.isEdit = true; this.state.isEdit = true;
this.state.isAuthEdit = true;
} }
this.savedFormValues = angular.copy(this.formValues); this.savedFormValues = angular.copy(this.formValues);

View File

@ -37,7 +37,13 @@
is-url-valid="true" is-url-valid="true"
stack-id="$ctrl.gitStackId" stack-id="$ctrl.gitStackId"
></git-form-ref-field> ></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"></git-form-auth-fieldset> <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>
<environment-variables-panel <environment-variables-panel
ng-model="$ctrl.formValues.Env" ng-model="$ctrl.formValues.Env"

View File

@ -56,7 +56,7 @@ export const gitFormModule = angular
'reactGitFormAuthFieldset', 'reactGitFormAuthFieldset',
r2a(withUIRouter(withReactQuery(withCurrentUser(AuthFieldset))), [ r2a(withUIRouter(withReactQuery(withCurrentUser(AuthFieldset))), [
'value', 'value',
'isExplanationVisible', 'isAuthExplanationVisible',
'onChange', 'onChange',
'errors', 'errors',
]) ])

View File

@ -18,14 +18,14 @@ import { NewCredentialForm } from './NewCredentialForm';
interface Props { interface Props {
value: GitAuthModel; value: GitAuthModel;
onChange: (value: Partial<GitAuthModel>) => void; onChange: (value: Partial<GitAuthModel>) => void;
isExplanationVisible?: boolean; isAuthExplanationVisible?: boolean;
errors?: FormikErrors<GitAuthModel>; errors?: FormikErrors<GitAuthModel>;
} }
export function AuthFieldset({ export function AuthFieldset({
value, value,
onChange, onChange,
isExplanationVisible, isAuthExplanationVisible,
errors, errors,
}: Props) { }: Props) {
const [username, setUsername] = useDebounce( const [username, setUsername] = useDebounce(
@ -56,7 +56,7 @@ export function AuthFieldset({
{value.RepositoryAuthentication && ( {value.RepositoryAuthentication && (
<> <>
{isExplanationVisible && ( {isAuthExplanationVisible && (
<TextTip color="orange"> <TextTip color="orange">
Enabling authentication will store the credentials and it is Enabling authentication will store the credentials and it is
advisable to use a git service account advisable to use a git service account
@ -143,7 +143,8 @@ export function AuthFieldset({
} }
export function gitAuthValidation( export function gitAuthValidation(
gitCredentials: Array<GitCredential> gitCredentials: Array<GitCredential>,
isAuthEdit: boolean
): SchemaOf<GitAuthModel> { ): SchemaOf<GitAuthModel> {
return object({ return object({
RepositoryAuthentication: boolean().default(false), RepositoryAuthentication: boolean().default(false),
@ -156,7 +157,7 @@ export function gitAuthValidation(
.default(''), .default(''),
RepositoryPassword: string() RepositoryPassword: string()
.when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], { .when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], {
is: (auth: boolean, id: number) => auth && !id, is: (auth: boolean, id: number) => auth && !id && !isAuthEdit,
then: string().required('Password is required'), then: string().required('Password is required'),
}) })
.default(''), .default(''),

View File

@ -51,7 +51,7 @@ export function GitForm({
<AuthFieldset <AuthFieldset
value={value} value={value}
onChange={handleChange} onChange={handleChange}
isExplanationVisible={isAuthExplanationVisible} isAuthExplanationVisible={isAuthExplanationVisible}
errors={errors} errors={errors}
/> />
@ -162,5 +162,5 @@ export function buildGitValidationSchema(
RepositoryURLValid: boolean().default(false), RepositoryURLValid: boolean().default(false),
AutoUpdate: autoUpdateValidation().nullable(), AutoUpdate: autoUpdateValidation().nullable(),
TLSSkipVerify: boolean().default(false), TLSSkipVerify: boolean().default(false),
}).concat(gitAuthValidation(gitCredentials)); }).concat(gitAuthValidation(gitCredentials, false));
} }