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
feat: create only non existing build time secrets

feat: alternative solution for list existing secret

test: add missging tests

fix: fix type errors

test: add coverage to missing lines
  • Loading branch information
JoaoPedroPP committed Nov 2, 2024
1 parent ec397ae commit eb0f30f
Show file tree
Hide file tree
Showing 12 changed files with 194 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 @@ -2,16 +2,17 @@ import React from 'react';
import { TextInputTypes, GridItem, Grid, FormSection } from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circle-icon';
import { useFormikContext } from 'formik';
import { Base64 } from 'js-base64';
import { useSecrets } from '../../../hooks/useSecrets';
import { SecretModel } from '../../../models';
import { InputField, TextColumnField } from '../../../shared';
import { ExistingSecret, SecretType } from '../../../types';
import { AccessReviewResources } from '../../../types/rbac';
import { useAccessReviewForModels } from '../../../utils/rbac';
import { useWorkspaceInfo } from '../../../utils/workspace-context-utils';
import { ButtonWithAccessTooltip } from '../../ButtonWithAccessTooltip';
import { useModalLauncher } from '../../modal/ModalProvider';
import { SecretModalLauncher } from '../../Secrets/SecretModalLauncher';
import { getSupportedPartnerTaskSecrets } from '../../Secrets/utils/secret-utils';
import { ImportFormValues } from '../type';

const accessReviewResources: AccessReviewResources = [{ model: SecretModel, verb: 'create' }];
Expand All @@ -24,12 +25,20 @@ const SecretSection = () => {

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

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
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import '@testing-library/jest-dom';
import { useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils';
import { screen, fireEvent, act, waitFor } from '@testing-library/react';
import { useSecrets } from '../../../../hooks/useSecrets';
import { useAccessReviewForModels } from '../../../../utils/rbac';
import { formikRenderer } from '../../../../utils/test-utils';
import SecretSection from '../SecretSection';
Expand All @@ -15,13 +16,43 @@ jest.mock('../../../../utils/rbac', () => ({
useAccessReviewForModels: jest.fn(),
}));

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

const watchResourceMock = useK8sWatchResource as jest.Mock;
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 with empy list of secrets', () => {
useSecretsMock.mockReturnValue([], false);
formikRenderer(<SecretSection />, {});

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

it('should render secret section', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/components/ImportForm/__tests__/submit-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createImageRepository,
} from '../../../utils/create-utils';
import { createIntegrationTest } from '../../IntegrationTest/IntegrationTestForm/utils/create-utils';
import { getSecretResource } from '../../Secrets/utils/secret-utils';
import { createResources } from '../submit-utils';

jest.mock('@redhat-cloud-services/frontend-components-notifications/redux');
Expand All @@ -19,10 +20,16 @@ jest.mock('../../IntegrationTest/IntegrationTestForm/utils/create-utils', () =>
createIntegrationTest: jest.fn(),
}));

jest.mock('../../Secrets/utils/secret-utils', () => ({
...(jest.requireActual('../../Secrets/utils/secret-utils') as object),
getSecretResource: jest.fn(),
}));

const createApplicationMock = createApplication as jest.Mock;
const createComponentMock = createComponent as jest.Mock;
const createIntegrationTestMock = createIntegrationTest as jest.Mock;
const createImageRepositoryMock = createImageRepository as jest.Mock;
const getSecretResourceMock = getSecretResource as jest.Mock;

describe('Submit Utils: createResources', () => {
it('should create application and components', async () => {
Expand All @@ -46,6 +53,7 @@ describe('Submit Utils: createResources', () => {
'test-ws',
'url.bombino',
);
expect(getSecretResourceMock).toHaveBeenCalledTimes(1);
expect(createApplicationMock).toHaveBeenCalledTimes(2);
expect(createIntegrationTestMock).toHaveBeenCalledTimes(2);
expect(createComponentMock).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -76,6 +84,7 @@ describe('Submit Utils: createResources', () => {
expect(createApplicationMock).toHaveBeenCalledTimes(2);
expect(createIntegrationTestMock).toHaveBeenCalledTimes(2);
expect(createComponentMock).toHaveBeenCalledTimes(0);
expect(getSecretResourceMock).toHaveBeenCalledTimes(0);
expect(createImageRepositoryMock).toHaveBeenCalledTimes(0);
});

Expand All @@ -102,6 +111,7 @@ describe('Submit Utils: createResources', () => {
);
expect(createApplicationMock).toHaveBeenCalledTimes(0);
expect(createIntegrationTestMock).toHaveBeenCalledTimes(0);
expect(getSecretResourceMock).toHaveBeenCalledTimes(1);
expect(createComponentMock).toHaveBeenCalledTimes(2);
expect(createImageRepositoryMock).toHaveBeenCalledTimes(2);
});
Expand Down
9 changes: 7 additions & 2 deletions src/components/ImportForm/submit-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getSecretResource } from '../../components/Secrets/utils/secret-utils';
import { ApplicationKind, ImportSecret } from '../../types';
import {
createApplication,
Expand Down Expand Up @@ -93,7 +94,11 @@ export const createResources = async (

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

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

return {
Expand Down
57 changes: 42 additions & 15 deletions src/components/Secrets/SecretForm.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,60 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Form } from '@patternfly/react-core';
import { SelectVariant } from '@patternfly/react-core/deprecated';
import { useFormikContext } from 'formik';
import { DropdownItemObject, SelectInputField } from '../../shared';
import KeyValueFileInputField from '../../shared/components/formik-fields/key-value-file-input-field/KeyValueFileInputField';
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 [options, setOptions] = React.useState([]);
const [optionsValues, setOptionsValues] = React.useState(null);

const initialOptions = getSupportedPartnerTaskSecrets().filter(
(secret) => !existingSecrets.includes(secret.value),
);
const [options, setOptions] = React.useState(initialOptions);
const currentTypeRef = React.useRef(values.type);
useEffect(() => {
const initialOptions = 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 }));
const initialOptionsValues = 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 },
);

setOptions(initialOptions);
setOptionsValues(initialOptionsValues);
}, [currentType, existingSecrets]);

const clearKeyValues = () => {
const newKeyValues = values.keyValues.filter((kv) => !kv.readOnlyKey);
Expand Down Expand Up @@ -54,14 +82,13 @@ 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) &&
setFieldValue('secretName', '');
} else {
setOptions(initialOptions);
clearKeyValues();
}
}}
Expand All @@ -80,15 +107,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={(e, value) => {
if (isPartnerTask(value)) {
if (isPartnerTask(value, optionsValues)) {
setFieldValue('keyValues', [
...values.keyValues.filter((kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value)),
...getSupportedPartnerTaskKeyValuePairs(value),
...getSupportedPartnerTaskKeyValuePairs(value, optionsValues),
]);
}
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?: any,
onSubmit?: (values: SecretFormValues) => void,
onClose?: () => void,
) =>
Expand Down
32 changes: 24 additions & 8 deletions src/components/Secrets/__tests___/SecretModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { SecretTypeDropdownLabel } from '../../../types';
import { SecretTypeDropdownLabel, SecretType } from '../../../types';
import { formikRenderer } from '../../../utils/test-utils';
import SecretModal, { SecretModalValues } from '../SecretModal';
import { supportedPartnerTasksSecrets } from '../utils/secret-utils';
Expand All @@ -13,11 +13,27 @@ const initialValues: SecretModalValues = {
existingSecrets: [],
};

const snykSecret = {
type: SecretType.opaque,
name: 'snyk-secret',
tokenKeyName: 'snyk-secret',
providerUrl: '',
keyValuePairs: [{ key: 'snyk_token', value: 'snyk_value', readOnlyKey: true }],
};

const testSecret = {
type: SecretType.opaque,
name: 'test-secret',
tokenKeyName: 'test-secret',
providerUrl: '',
keyValuePairs: [{ key: 'test_token', value: 'test_value', readOnlyKey: true }],
};

describe('SecretForm', () => {
it('should show secret form in a modal', async () => {
formikRenderer(
<SecretModal
existingSecrets={['test']}
existingSecrets={[testSecret]}
onSubmit={jest.fn()}
modalProps={{ isOpen: true, onClose: jest.fn() }}
/>,
Expand All @@ -32,7 +48,7 @@ describe('SecretForm', () => {
it('should render validation message when user click on create button without filling the form', async () => {
formikRenderer(
<SecretModal
existingSecrets={['test']}
existingSecrets={[testSecret]}
onSubmit={jest.fn()}
modalProps={{ isOpen: true, onClose: jest.fn() }}
/>,
Expand All @@ -50,7 +66,7 @@ describe('SecretForm', () => {
formikRenderer(
<SecretModal
onSubmit={jest.fn()}
existingSecrets={['test']}
existingSecrets={[testSecret]}
modalProps={{ isOpen: true, onClose }}
/>,
initialValues,
Expand All @@ -69,7 +85,7 @@ describe('SecretForm', () => {
formikRenderer(
<SecretModal
onSubmit={jest.fn()}
existingSecrets={['test']}
existingSecrets={[testSecret]}
modalProps={{ isOpen: true, onClose }}
/>,
initialValues,
Expand All @@ -88,12 +104,12 @@ describe('SecretForm', () => {
});
});

it('should not show the secrets in the select dropdown if it is already existing', async () => {
it('should show the secrets in the select dropdown if it is already existing', async () => {
const onClose = jest.fn();
formikRenderer(
<SecretModal
onSubmit={jest.fn()}
existingSecrets={['snyk-secret']}
existingSecrets={[snykSecret]}
modalProps={{ isOpen: true, onClose }}
/>,
initialValues,
Expand All @@ -104,7 +120,7 @@ describe('SecretForm', () => {
const modal = screen.queryByTestId('build-secret-modal');
fireEvent.click(modal.querySelector('#secret-name-toggle-select-typeahead'));
});
expect(screen.queryByText('snyk-secret')).not.toBeInTheDocument();
expect(screen.queryByText('snyk-secret')).toBeInTheDocument();
});

it('should remove the selected value with clearn button is clicked', async () => {
Expand Down
Loading

0 comments on commit eb0f30f

Please sign in to comment.