Skip to content

Commit

Permalink
fix: list existing secret in build time secret modal
Browse files Browse the repository at this point in the history
  • Loading branch information
JoaoPedroPP committed Jan 6, 2025
1 parent 69a23b0 commit f73cf3a
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 53 deletions.
21 changes: 15 additions & 6 deletions src/components/ImportForm/SecretSection/SecretSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { TextInputTypes, GridItem, Grid, FormSection } from '@patternfly/react-c
import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circle-icon';
import { useFormikContext } from 'formik';
import { InputField } from 'formik-pf';
import { Base64 } from 'js-base64';
import { useSecrets } from '../../../hooks/useSecrets';
import { SecretModel } from '../../../models';
import TextColumnField from '../../../shared/components/formik-fields/text-column-field/TextColumnField';
import { ExistingSecret, SecretType } from '../../../types';
import { AccessReviewResources } from '../../../types/rbac';
import { useAccessReviewForModels } from '../../../utils/rbac';
import { ButtonWithAccessTooltip } from '../../ButtonWithAccessTooltip';
import { useModalLauncher } from '../../modal/ModalProvider';
import { SecretModalLauncher } from '../../Secrets/SecretModalLauncher';
import { getSupportedPartnerTaskSecrets } from '../../Secrets/utils/secret-utils';
import { useWorkspaceInfo } from '../../Workspace/useWorkspaceInfo';
import { ImportFormValues } from '../type';

Expand All @@ -25,12 +26,20 @@ const SecretSection = () => {

const [secrets, secretsLoaded] = useSecrets(namespace, workspace);

const partnerTaskNames = getSupportedPartnerTaskSecrets().map(({ label }) => label);
const partnerTaskSecrets: string[] =
const partnerTaskSecrets: ExistingSecret[] =
secrets && secretsLoaded
? secrets
?.filter((rs) => partnerTaskNames.includes(rs.metadata.name))
?.map((s) => s.metadata.name) || []
? secrets?.map((secret) => ({
type: secret.type as SecretType,
name: secret.metadata.name,
providerUrl: '',
tokenKeyName: secret.metadata.name,
keyValuePairs: Object.keys(secret.data).map((key) => ({
key,
value: Base64.decode(secret.data[key]),
readOnlyKey: true,
readOnlyValue: true,
})),
}))
: [];

const onSubmit = React.useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { screen, fireEvent, act, waitFor } from '@testing-library/react';
import { useSecrets } from '../../../../hooks/useSecrets';
import { useAccessReviewForModels } from '../../../../utils/rbac';
import { createK8sWatchResourceMock, formikRenderer } from '../../../../utils/test-utils';
import SecretSection from '../SecretSection';
Expand All @@ -7,13 +8,35 @@ jest.mock('../../../../utils/rbac', () => ({
useAccessReviewForModels: jest.fn(),
}));

jest.mock('../../../../hooks/useSecrets', () => ({
useSecrets: jest.fn(),
}));

const watchResourceMock = createK8sWatchResourceMock();
const accessReviewMock = useAccessReviewForModels as jest.Mock;
const useSecretsMock = useSecrets as jest.Mock;

describe('SecretSection', () => {
beforeEach(() => {
watchResourceMock.mockReturnValue([[], true]);
accessReviewMock.mockReturnValue([true, true]);
useSecretsMock.mockReturnValue([
[
{
metadata: {
name: 'snyk-secret',
namespace: 'test-ws',
},
data: {
'snyk-token': 'c255ay1zZWNyZXQ=',
},
type: 'Opaque',
apiVersion: 'v1',
kind: 'Secret',
},
],
true,
]);
});

it('should render secret section', () => {
Expand All @@ -23,6 +46,22 @@ describe('SecretSection', () => {
screen.getByTestId('add-secret-button');
});

it('should render secret section, secret do not load yet', () => {
useSecretsMock.mockReturnValue([[], false]);
formikRenderer(<SecretSection />, {});

screen.getByText('Build time secret');
screen.getByTestId('add-secret-button');
});

it('should render secret section with empty list of secrets', () => {
useSecretsMock.mockReturnValue([[], true]);
formikRenderer(<SecretSection />, {});

screen.getByText('Build time secret');
screen.getByTestId('add-secret-button');
});

it('should render added secrets in removable lists', () => {
formikRenderer(<SecretSection />, { newSecrets: ['secret-one', 'secret-two'] });

Expand Down
53 changes: 52 additions & 1 deletion src/components/ImportForm/__tests__/submit-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SecretType } from '../../../types';
import {
createApplication,
createComponent,
Expand Down Expand Up @@ -77,7 +78,7 @@ describe('Submit Utils: createResources', () => {
expect(createImageRepositoryMock).toHaveBeenCalledTimes(0);
});

it('should not create application but create components', async () => {
it('should not create application but create components without secrets', async () => {
createApplicationMock.mockResolvedValue({ metadata: { name: 'test-app' } });
createComponentMock.mockResolvedValue({ metadata: { name: 'test-component' } });
await createResources(
Expand All @@ -93,6 +94,56 @@ describe('Submit Utils: createResources', () => {
},
pipeline: 'dbcd',
componentName: 'component',
importSecrets: [
{
existingSecrets: [
{
name: 'secret',
type: SecretType.opaque,
providerUrl: '',
tokenKeyName: 'secret',
keyValuePairs: [
{
key: 'secret',
value: 'value',
readOnlyKey: true,
},
],
},
],
type: 'Opaque',
secretName: 'secret',
keyValues: [{ key: 'secret', value: 'test-value', readOnlyKey: true }],
},
],
},
'test-ws-tenant',
'test-ws',
'url.bombino',
);
expect(createApplicationMock).toHaveBeenCalledTimes(0);
expect(createIntegrationTestMock).toHaveBeenCalledTimes(0);
expect(createComponentMock).toHaveBeenCalledTimes(2);
expect(createImageRepositoryMock).toHaveBeenCalledTimes(2);
});

it('should not create application, create components and secret', async () => {
createApplicationMock.mockResolvedValue({ metadata: { name: 'test-app' } });
createComponentMock.mockResolvedValue({ metadata: { name: 'test-component' } });
await createResources(
{
application: 'test-app',
inAppContext: true,
showComponent: true,
isPrivateRepo: false,
source: {
git: {
url: 'https://github.com/',
},
},
pipeline: 'dbcd',
componentName: 'component',
importSecrets: [],
},
'test-ws-tenant',
'test-ws',
Expand Down
7 changes: 5 additions & 2 deletions src/components/ImportForm/submit-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ export const createResources = async (

let createdComponent;
if (showComponent) {
await createSecrets(importSecrets, workspace, namespace, true);
const secretsToCreate = importSecrets.filter((secret) =>
secret.existingSecrets.find((existing) => secret.secretName === existing.name) ? false : true,
);
await createSecrets(secretsToCreate, workspace, namespace, true);

createdComponent = await createComponent(
{ componentName, application, gitProviderAnnotation, source, gitURLAnnotation },
Expand All @@ -123,7 +126,7 @@ export const createResources = async (
isPrivate: isPrivateRepo,
bombinoUrl,
});
await createSecrets(importSecrets, workspace, namespace, false);
await createSecrets(secretsToCreate, workspace, namespace, false);
}

return {
Expand Down
4 changes: 2 additions & 2 deletions src/components/ImportForm/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ImportSecret } from '../../types';
import { SecretFormValues } from '../../types';

export type ImportFormValues = {
application: string;
Expand All @@ -17,6 +17,6 @@ export type ImportFormValues = {
};
};
pipeline: string;
importSecrets?: ImportSecret[];
importSecrets?: SecretFormValues[];
newSecrets?: string[];
};
55 changes: 40 additions & 15 deletions src/components/Secrets/SecretForm.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Form } from '@patternfly/react-core';
import { SelectVariant } from '@patternfly/react-core/deprecated';
import { useFormikContext } from 'formik';
import { DropdownItemObject } from '../../shared/components/dropdown';
import KeyValueFileInputField from '../../shared/components/formik-fields/key-value-file-input-field/KeyValueFileInputField';
import SelectInputField from '../../shared/components/formik-fields/SelectInputField';
import { SecretFormValues, SecretTypeDropdownLabel } from '../../types';
import {
SecretFormValues,
SecretTypeDropdownLabel,
K8sSecretType,
ExistingSecret,
} from '../../types';
import { RawComponentProps } from '../modal/createModalLauncher';
import SecretTypeSelector from './SecretTypeSelector';
import {
supportedPartnerTasksSecrets,
getSupportedPartnerTaskKeyValuePairs,
isPartnerTask,
getSupportedPartnerTaskSecrets,
} from './utils/secret-utils';

type SecretFormProps = RawComponentProps & {
existingSecrets: string[];
existingSecrets: ExistingSecret[];
};

const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existingSecrets }) => {
const { values, setFieldValue } = useFormikContext<SecretFormValues>();
const [currentType, setType] = React.useState(values.type);
const defaultKeyValues = [{ key: '', value: '', readOnlyKey: false }];
const defaultImageKeyValues = [{ key: '.dockerconfigjson', value: '', readOnlyKey: true }];

const initialOptions = getSupportedPartnerTaskSecrets().filter(
(secret) => !existingSecrets.includes(secret.value),
);
const [options, setOptions] = React.useState(initialOptions);
const currentTypeRef = React.useRef(values.type);
let options = useMemo(() => {
return existingSecrets
.filter((secret) => secret.type === K8sSecretType[currentType])
.concat(
currentType === SecretTypeDropdownLabel.opaque &&
existingSecrets.find((s) => s.name === 'snyk-secret') === undefined
? [supportedPartnerTasksSecrets.snyk]
: [],
)
.filter((secret) => secret.type !== K8sSecretType[SecretTypeDropdownLabel.image])
.map((secret) => ({ value: secret.name, lable: secret.name }));
}, [currentType, existingSecrets]);
const optionsValues = useMemo(() => {
return existingSecrets
.filter((secret) => secret.type === K8sSecretType[currentType])
.filter((secret) => secret.type !== K8sSecretType[SecretTypeDropdownLabel.image])
.reduce(
(dictOfSecrets, secret) => {
dictOfSecrets[secret.name] = secret;
return dictOfSecrets;
},
{ 'snyk-secret': supportedPartnerTasksSecrets.snyk },
);
}, [currentType, existingSecrets]);

const clearKeyValues = () => {
const newKeyValues = values.keyValues.filter((kv) => !kv.readOnlyKey);
void setFieldValue('keyValues', [...(newKeyValues.length ? newKeyValues : defaultKeyValues)]);
};

const resetKeyValues = () => {
setOptions([]);
options = [];
const newKeyValues = values.keyValues.filter(
(kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value),
);
Expand All @@ -55,11 +80,11 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
<SecretTypeSelector
dropdownItems={dropdownItems}
onChange={(type) => {
currentTypeRef.current = type;
setType(type);
if (type === SecretTypeDropdownLabel.image) {
resetKeyValues();
values.secretName &&
isPartnerTask(values.secretName) &&
isPartnerTask(values.secretName, optionsValues) &&
void setFieldValue('secretName', '');
} else {
setOptions(initialOptions);
Expand All @@ -81,15 +106,15 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
toggleId="secret-name-toggle"
toggleAriaLabel="secret-name-dropdown"
onClear={() => {
if (currentTypeRef.current !== values.type || isPartnerTask(values.secretName)) {
if (currentType !== values.type || isPartnerTask(values.secretName, optionsValues)) {
clearKeyValues();
}
}}
onSelect={(_, value: string) => {
if (isPartnerTask(value)) {
if (isPartnerTask(value, optionsValues)) {
void setFieldValue('keyValues', [
...values.keyValues.filter((kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value)),
...getSupportedPartnerTaskKeyValuePairs(value),
...getSupportedPartnerTaskKeyValuePairs(value, optionsValues),
]);
}
void setFieldValue('secretName', value);
Expand Down
6 changes: 3 additions & 3 deletions src/components/Secrets/SecretModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ModalVariant,
} from '@patternfly/react-core';
import { Formik } from 'formik';
import { ImportSecret, SecretTypeDropdownLabel } from '../../types';
import { ImportSecret, SecretTypeDropdownLabel, ExistingSecret } from '../../types';
import { SecretFromSchema } from '../../utils/validation-utils';
import { RawComponentProps } from '../modal/createModalLauncher';
import SecretForm from './SecretForm';
Expand All @@ -25,11 +25,11 @@ const createPartnerTaskSecret = (
};

export type SecretModalValues = ImportSecret & {
existingSecrets: string[];
existingSecrets: ExistingSecret[];
};

type SecretModalProps = RawComponentProps & {
existingSecrets: string[];
existingSecrets: ExistingSecret[];
onSubmit: (value: SecretModalValues) => void;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/Secrets/SecretModalLauncher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createRawModalLauncher } from '../modal/createModalLauncher';
import SecretForm from './SecretModal';

export const SecretModalLauncher = (
existingSecrets?: string[],
existingSecrets?: ExistingSecret[],
onSubmit?: (values: SecretFormValues) => void,
onClose?: () => void,
) =>
Expand Down
Loading

0 comments on commit f73cf3a

Please sign in to comment.