Skip to content

Commit

Permalink
feat(secret): add source secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
abhinandan13jan committed Jan 15, 2025
1 parent 88eafd6 commit fdf1db5
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 44 deletions.
37 changes: 27 additions & 10 deletions src/components/Secrets/SecretForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import KeyValueFileInputField from '../../shared/components/formik-fields/key-va
import SelectInputField from '../../shared/components/formik-fields/SelectInputField';
import { SecretFormValues, SecretTypeDropdownLabel } from '../../types';
import { RawComponentProps } from '../modal/createModalLauncher';
import { SourceSecretForm } from './SecretsForm/SourceSecretForm';
import SecretTypeSelector from './SecretTypeSelector';
import {
getSupportedPartnerTaskKeyValuePairs,
Expand All @@ -30,26 +31,28 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
const currentTypeRef = React.useRef(values.type);

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

const resetKeyValues = () => {
setOptions([]);
const newKeyValues = values.keyValues.filter(
const newKeyValues = values.opaque.keyValues.filter(
(kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value),
);
void setFieldValue('keyValues', [...newKeyValues, ...defaultImageKeyValues]);
};

const dropdownItems: DropdownItemObject[] = Object.entries(SecretTypeDropdownLabel).reduce(
(acc, [key, value]) => {
value !== SecretTypeDropdownLabel.source && acc.push({ key, value });
acc.push({ key, value });
return acc;
},
[],
);

const currentType = currentTypeRef.current;

return (
<Form>
<SecretTypeSelector
Expand All @@ -61,6 +64,12 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
values.secretName &&
isPartnerTask(values.secretName) &&
void setFieldValue('secretName', '');
}
if (type === SecretTypeDropdownLabel.source) {
resetKeyValues();
values.secretName &&
isPartnerTask(values.secretName) &&
void setFieldValue('secretName', '');
} else {
setOptions(initialOptions);
clearKeyValues();
Expand Down Expand Up @@ -88,19 +97,27 @@ const SecretForm: React.FC<React.PropsWithChildren<SecretFormProps>> = ({ existi
onSelect={(_, value: string) => {
if (isPartnerTask(value)) {
void setFieldValue('keyValues', [
...values.keyValues.filter((kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value)),
...values.opaque.keyValues.filter(
(kv) => !kv.readOnlyKey && (!!kv.key || !!kv.value),
),
...getSupportedPartnerTaskKeyValuePairs(value),
]);
}
void setFieldValue('secretName', value);
}}
/>
<KeyValueFileInputField
name="keyValues"
data-test="secret-key-value-pair"
entries={defaultKeyValues}
disableRemoveAction={values.keyValues.length === 1}
/>
{currentType === SecretTypeDropdownLabel.source && <SourceSecretForm />}
{currentType !== SecretTypeDropdownLabel.source && (
<KeyValueFileInputField
required
name={
currentType === SecretTypeDropdownLabel.opaque ? 'opaque.keyValues' : 'image.keyValues'
}
data-test="secret-key-value-pair"
entries={defaultKeyValues}
disableRemoveAction={values.opaque.keyValues.length === 1}
/>
)}
</Form>
);
};
Expand Down
14 changes: 12 additions & 2 deletions src/components/Secrets/SecretModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
ModalVariant,
} from '@patternfly/react-core';
import { Formik } from 'formik';
import { ImportSecret, SecretTypeDropdownLabel } from '../../types';
import { isEmpty } from 'lodash-es';
import { ImportSecret, SecretTypeDropdownLabel, SourceSecretType } from '../../types';
import { SecretFromSchema } from '../../utils/validation-utils';
import { RawComponentProps } from '../modal/createModalLauncher';
import SecretForm from './SecretForm';
Expand Down Expand Up @@ -42,7 +43,15 @@ const SecretModal: React.FC<React.PropsWithChildren<SecretModalProps>> = ({
const initialValues: SecretModalValues = {
secretName: '',
type: SecretTypeDropdownLabel.opaque,
keyValues: defaultKeyValues,
opaque: {
keyValues: defaultKeyValues,
},
image: {
keyValues: defaultKeyValues,
},
source: {
authType: SourceSecretType.basic,
},
existingSecrets,
};

Expand All @@ -68,6 +77,7 @@ const SecretModal: React.FC<React.PropsWithChildren<SecretModalProps>> = ({
onClick={() => {
props.handleSubmit();
}}
isDisabled={!props.dirty || !isEmpty(props.errors) || props.isSubmitting}
>
Create
</Button>,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Secrets/__tests___/SecretModal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { supportedPartnerTasksSecrets } from '../utils/secret-utils';
const initialValues: SecretModalValues = {
secretName: '',
type: SecretTypeDropdownLabel.opaque,
keyValues: [{ key: '', value: '', readOnlyKey: false }],
opaque: { keyValues: [{ key: '', value: '', readOnlyKey: false }] },
existingSecrets: [],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SecretTypeSelector from '../SecretTypeSelector';
const initialValues: SecretModalValues = {
secretName: '',
type: SecretTypeDropdownLabel.opaque,
keyValues: [{ key: '', value: '', readOnlyKey: false }],
opaque: { keyValues: [{ key: '', value: '', readOnlyKey: false }] },
existingSecrets: [],
};

Expand Down
20 changes: 15 additions & 5 deletions src/types/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ export const SecretByUILabel = 'ui.appstudio.redhat.com/secret-for';
export type ImportSecret = {
secretName: string;
type: string;
keyValues: {
key: string;
value: string;
readOnlyKey?: boolean;
}[];
source?: Source;
opaque?: {
keyValues: {
key: string;
value: string;
readOnlyKey?: boolean;
}[];
};
image?: {
keyValues: {
key: string;
value: string;
readOnlyKey?: boolean;
}[];
};
};

export enum SecretSPILabel {
Expand Down
90 changes: 85 additions & 5 deletions src/utils/__tests__/create-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { commonFetch } from '../../k8s/fetch';
import { k8sCreateResource, k8sUpdateResource } from '../../k8s/k8s-fetch';
import { ApplicationModel } from '../../models/application';
import { ComponentModel } from '../../models/component';
import { AddSecretFormValues, SecretFor, SecretTypeDropdownLabel } from '../../types';
import {
AddSecretFormValues,
SecretFor,
SecretTypeDropdownLabel,
SourceSecretType,
} from '../../types';
import { ComponentKind, ComponentSpecs } from '../../types/component';
import {
createApplication,
Expand Down Expand Up @@ -467,7 +472,7 @@ describe('Create Utils', () => {
{
secretName: 'my-snyk-secret',
type: SecretTypeDropdownLabel.opaque,
keyValues: [{ key: 'token', value: 'my-token-data' }],
opaque: { keyValues: [{ key: 'token', value: 'my-token-data' }] },
},
'test-ws',
'test-ns',
Expand All @@ -492,7 +497,7 @@ describe('Create Utils', () => {
{
secretName: 'my-snyk-secret',
type: SecretTypeDropdownLabel.opaque,
keyValues: [{ key: 'token', value: 'my-token-data' }],
opaque: { keyValues: [{ key: 'token', value: 'my-token-data' }] },
},
'test-ws',
'test-ns',
Expand All @@ -517,7 +522,7 @@ describe('Create Utils', () => {
{
secretName: 'registry-creds',
type: SecretTypeDropdownLabel.image,
keyValues: [{ key: 'token', value: 'my-token-data' }],
image: { keyValues: [{ key: 'token', value: 'my-token-data' }] },
},
'test-ws',
'test-ns',
Expand All @@ -534,6 +539,81 @@ describe('Create Utils', () => {
);
});

it('should add correct values for Image pull secret', async () => {
commonFetchMock.mockClear();
commonFetchMock.mockImplementationOnce((props) => Promise.resolve(props));

await createSecret(
{
secretName: 'registry-creds',
type: SecretTypeDropdownLabel.image,
image: { keyValues: [{ key: 'test', value: 'test-value' }] },
},
'test-ws',
'test-ns',
false,
);

expect(commonFetchMock).toHaveBeenCalledTimes(1);

expect(commonFetchMock).toHaveBeenCalledWith(
'/workspaces/test-ws/api/v1/namespaces/test-ns/secrets',
expect.objectContaining({
body: expect.stringContaining('"test":"test-value"'),
}),
);
});

it('should create a Source secret', async () => {
commonFetchMock.mockClear();
commonFetchMock.mockImplementationOnce((props) => Promise.resolve(props));

await createSecret(
{
secretName: 'registry-creds',
type: SecretTypeDropdownLabel.source,
source: { authType: SourceSecretType.basic, username: 'test1', password: 'pass-test' },
},
'test-ws',
'test-ns',
false,
);

expect(commonFetchMock).toHaveBeenCalledTimes(1);

expect(commonFetchMock).toHaveBeenCalledWith(
'/workspaces/test-ws/api/v1/namespaces/test-ns/secrets',
expect.objectContaining({
body: expect.stringContaining('"type":"kubernetes.io/basic-auth"'),
}),
);
});

it('should add correct data for Source secret', async () => {
commonFetchMock.mockClear();
commonFetchMock.mockImplementationOnce((props) => Promise.resolve(props));

await createSecret(
{
secretName: 'registry-creds',
type: SecretTypeDropdownLabel.source,
source: { authType: SourceSecretType.basic, username: 'test1', password: 'pass-test' },
},
'test-ws',
'test-ns',
false,
);

expect(commonFetchMock).toHaveBeenCalledTimes(1);

expect(commonFetchMock).toHaveBeenCalledWith(
'/workspaces/test-ws/api/v1/namespaces/test-ns/secrets',
expect.objectContaining({
body: expect.stringContaining('"username":"dGVzdDE=","password":"cGFzcy10ZXN0"'),
}),
);
});

it('should create partner task secret', async () => {
commonFetchMock.mockClear();
createResourceMock
Expand All @@ -545,7 +625,7 @@ describe('Create Utils', () => {
{
secretName: 'snyk-secret',
type: SecretTypeDropdownLabel.opaque,
keyValues: [{ key: 'token', value: 'my-token-data' }],
opaque: { keyValues: [{ key: 'token', value: 'my-token-data' }] },
},
'test-ws',
'test-ns',
Expand Down
61 changes: 46 additions & 15 deletions src/utils/create-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isEqual, isNumber } from 'lodash-es';
import { Base64 } from 'js-base64';
import { isEqual, isNumber, pick } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid';
import { getRandomSvgNumber, THUMBNAIL_ANNOTATION } from '../components/ApplicationThumbnail';
import {
Expand Down Expand Up @@ -31,6 +32,9 @@ import {
ImportSecret,
ImageRepositoryKind,
ImageRepositoryVisibility,
SecretTypeDropdownLabel,
SourceSecretType,
SecretFormValues,
} from '../types';
import { ComponentSpecs } from './../types/component';
import {
Expand All @@ -39,7 +43,6 @@ import {
GIT_PROVIDER_ANNOTATION,
GITLAB_PROVIDER_URL_ANNOTATION,
} from './component-utils';

export const sanitizeName = (name: string) => name.split(/ |\./).join('-').toLowerCase();

/**
Expand Down Expand Up @@ -316,6 +319,46 @@ export const initiateAccessTokenBinding = async (url: string, namespace: string)
return createAccessTokenBinding(url, namespace);
};

export const getSecretObject = (values: SecretFormValues, namespace: string): SecretKind => {
let data = {};
if (values.type === SecretTypeDropdownLabel.source) {
if (values.source.authType === SourceSecretType.basic) {
const authObj = pick(values.source, ['username', 'password']);
data = Object.entries(authObj).reduce((acc, [key, value]) => {
acc[key] = Base64.encode(value);
return acc;
}, {});
} else {
const SSH_KEY = 'ssh-privatekey';
data[SSH_KEY] = values.source[SSH_KEY];
}
} else {
const keyValues =
values.type === SecretTypeDropdownLabel.opaque
? values.opaque.keyValues
: values.image.keyValues;
data = keyValues?.reduce((acc, s) => {
acc[s.key] = s.value ? s.value : '';
return acc;
}, {});
}
const secretResource: SecretKind = {
apiVersion: SecretModel.apiVersion,
kind: SecretModel.kind,
metadata: {
name: values.secretName,
namespace,
},
type:
values.type === SecretTypeDropdownLabel.source
? K8sSecretType[values.source?.authType]
: K8sSecretType[values.type],
stringData: data,
};

return secretResource;
};

export const createSecretResource = async (
values: AddSecretFormValues,
workspace: string,
Expand Down Expand Up @@ -369,19 +412,7 @@ export const createSecret = async (
namespace: string,
dryRun: boolean,
) => {
const secretResource = {
apiVersion: SecretModel.apiVersion,
kind: SecretModel.kind,
metadata: {
name: secret.secretName,
namespace,
},
type: K8sSecretType[secret.type],
stringData: secret.keyValues.reduce((acc, s) => {
acc[s.key] = s.value ? s.value : '';
return acc;
}, {}),
};
const secretResource = getSecretObject(secret, namespace);

// Todo: K8sCreateResource appends the resource name and errors out.
// Fix the below code when this sdk-utils issue is resolved https://issues.redhat.com/browse/RHCLOUD-21655.
Expand Down
Loading

0 comments on commit fdf1db5

Please sign in to comment.