Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(KFLUXBUGS-1342): add existing secret in the components secret form #1006

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 empty 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) =>
Copy link

@StanislavJochman StanislavJochman Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it secure to fetch all secrets and than filter them? CC: @caugello

Copy link
Contributor Author

@JoaoPedroPP JoaoPedroPP Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch services was not necessary here and I removed, however it was required to fetch the secrets in other part of the code to create the list of secrets. I realize that we can filter if we need to create a new secret by filtering from the initial list, which is the currently PR.

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 @@
<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 @@
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