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

chore: remove empty array

test: add tests for submit-utils.ts

feat: improve method to select secrets to create
  • Loading branch information
JoaoPedroPP committed Nov 11, 2024
1 parent f7a700f commit 03cf690
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 55 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,51 @@ 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, 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 empy list of secrets', () => {
useSecretsMock.mockReturnValue([[], true]);
formikRenderer(<SecretSection />, {});

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

it('should render secret section', () => {
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 @@ -79,7 +80,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 @@ -95,6 +96,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 @@ -93,7 +93,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 @@ -113,7 +116,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[];
};
56 changes: 40 additions & 16 deletions src/components/Secrets/SecretForm.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,65 @@
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, 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 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);
setFieldValue('keyValues', [...(newKeyValues.length ? newKeyValues : defaultKeyValues)]);
};

const resetKeyValues = () => {
setOptions([]);
options = [];

Check warning on line 62 in src/components/Secrets/SecretForm.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Secrets/SecretForm.tsx#L62

Added line #L62 was not covered by tests
const newKeyValues = values.keyValues.filter(
(kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value),
);
Expand All @@ -54,14 +79,13 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
<SecretTypeSelector
dropdownItems={dropdownItems}
onChange={(type) => {
currentTypeRef.current = type;
setType(type);

Check warning on line 82 in src/components/Secrets/SecretForm.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Secrets/SecretForm.tsx#L82

Added line #L82 was not covered by tests
if (type === SecretTypeDropdownLabel.image) {
resetKeyValues();
values.secretName &&
isPartnerTask(values.secretName) &&
isPartnerTask(values.secretName, optionsValues) &&

Check warning on line 86 in src/components/Secrets/SecretForm.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Secrets/SecretForm.tsx#L86

Added line #L86 was not covered by tests
setFieldValue('secretName', '');
} else {
setOptions(initialOptions);
clearKeyValues();
}
}}
Expand All @@ -80,15 +104,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
Loading

0 comments on commit 03cf690

Please sign in to comment.