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

RFE-6265: Allow providing username & password for registry #2300

Merged
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
3 changes: 2 additions & 1 deletion src/utils/components/ExportModal/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { FC, useState } from 'react';
import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { modelToGroupVersionKind, PodModel } from '@kubevirt-utils/models';
import { createSecret } from '@kubevirt-utils/resources/secret/utils';
import { getName, getNamespace } from '@kubevirt-utils/resources/shared';
import { getRandomChars } from '@kubevirt-utils/utils/utils';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
Expand All @@ -19,7 +20,7 @@ import TabModal from '../TabModal/TabModal';

import { ALREADY_CREATED_ERROR_CODE } from './constants';
import ShowProgress from './ShowProgress';
import { createSecret, createServiceAccount, createUploaderPod, exportInProgress } from './utils';
import { createServiceAccount, createUploaderPod, exportInProgress } from './utils';
import ViewPodLogLink from './ViewPodLogLink';

import './export-modal.scss';
Expand Down
37 changes: 2 additions & 35 deletions src/utils/components/ExportModal/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { IoK8sApiCoreV1Pod, IoK8sApiCoreV1Secret } from '@kubevirt-ui/kubevirt-api/kubernetes';
import {
PodModel,
RoleBindingModel,
RoleModel,
SecretModel,
ServiceAccountModel,
} from '@kubevirt-utils/models';
import { encodeSecretKey } from '@kubevirt-utils/resources/secret/utils';
import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { PodModel, RoleBindingModel, RoleModel, ServiceAccountModel } from '@kubevirt-utils/models';
import { getRandomChars, isEmpty, isUpstream } from '@kubevirt-utils/utils/utils';
import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk';

Expand All @@ -19,32 +12,6 @@ import {
UPSTREAM_UPLOADER_IMAGE,
} from './constants';

type CreateSecretType = (input: {
namespace: string;
password: string;
secretName: string;
username: string;
}) => Promise<IoK8sApiCoreV1Secret>;

export const createSecret: CreateSecretType = ({ namespace, password, secretName, username }) =>
k8sCreate({
data: {
apiVersion: 'v1',
data: {
accessKeyId: encodeSecretKey(username),
secretKey: encodeSecretKey(password),
},
kind: 'Secret',
metadata: {
name: secretName,
namespace,
},
type: 'Opaque',
},
model: SecretModel,
ns: namespace,
});

export const createServiceAccount = async (namespace: string) => {
await Promise.all([
k8sCreate({ data: serviceAccount, model: ServiceAccountModel, ns: namespace }),
Expand Down
36 changes: 36 additions & 0 deletions src/utils/components/FormPasswordInput/FormPasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { ComponentProps, forwardRef, HTMLProps, useState } from 'react';

import { Button, ButtonVariant, Split, TextInput, TextInputProps } from '@patternfly/react-core';
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';

// PatternFly changed the signature of the 'onChange' handler for input elements.
// This causes issues with React Hook Form as it expects the default signature for an input element.
// So we have to create this wrapper component that takes care of converting these signatures for us.

export type FormPasswordInputProps = Omit<ComponentProps<typeof TextInput>, 'onChange'> &
Pick<HTMLProps<HTMLInputElement>, 'onChange'>;

export const FormPasswordInput = forwardRef<HTMLInputElement, FormPasswordInputProps>(
({ onChange, ...props }, ref) => {
const [passwordHidden, setPasswordHidden] = useState(true);
const onChangeForward: TextInputProps['onChange'] = (event) => onChange?.(event);

return (
<Split>
<TextInput
{...props}
onChange={(event, _value) => onChangeForward(event, _value)}
ref={ref}
type={passwordHidden ? 'password' : 'text'}
/>
<Button onClick={() => setPasswordHidden(!passwordHidden)} variant={ButtonVariant.link}>
{passwordHidden ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</Split>
);
},
);

// We need to fake the displayName to match what PatternFly expects.
// This is because PatternFly uses it to filter children in certain aspects.
FormPasswordInput.displayName = 'TextInput';
29 changes: 28 additions & 1 deletion src/utils/resources/secret/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Buffer } from 'buffer';

import { SecretModel } from '@kubevirt-ui/kubevirt-api/console';
import { IoK8sApiCoreV1Secret } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import {
SecretSelectionOption,
SSHSecretDetails,
} from '@kubevirt-utils/components/SSHSecretModal/utils/types';
import { SecretModel } from '@kubevirt-utils/models';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk';

import { getName } from '../shared';

Expand Down Expand Up @@ -67,3 +68,29 @@ export const getInitialSSHDetails = ({
sshSecretName: sshSecretName || '',
sshSecretNamespace: '',
};

type CreateSecretType = (input: {
namespace: string;
password: string;
secretName: string;
username: string;
}) => Promise<IoK8sApiCoreV1Secret>;

export const createSecret: CreateSecretType = ({ namespace, password, secretName, username }) =>
k8sCreate({
data: {
apiVersion: 'v1',
data: {
accessKeyId: encodeSecretKey(username),
secretKey: encodeSecretKey(password),
},
kind: 'Secret',
metadata: {
name: secretName,
namespace,
},
type: 'Opaque',
},
model: SecretModel,
ns: namespace,
});
9 changes: 8 additions & 1 deletion src/utils/resources/template/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TemplateParameter, V1Template } from '@kubevirt-ui/kubevirt-api/console
import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { getAnnotation } from '@kubevirt-utils/resources/shared';
import { getRootDataVolumeTemplateSpec } from '@kubevirt-utils/resources/vm';
import { generatePrettyName } from '@kubevirt-utils/utils/utils';

import { ANNOTATIONS } from './annotations';
Expand All @@ -12,7 +13,7 @@ import {
TEMPLATE_TYPE_BASE,
TEMPLATE_TYPE_LABEL,
} from './constants';
import { getTemplatePVCName } from './selectors';
import { getTemplatePVCName, getTemplateVirtualMachineObject } from './selectors';

// Only used for replacing parameters in the template, do not use for anything else
// eslint-disable-next-line require-jsdoc
Expand Down Expand Up @@ -70,3 +71,9 @@ export const generateParamsWithPrettyName = (template: V1Template) => {
}
return [];
};

export const bootDiskSourceIsRegistry = (template: V1Template) => {
const vmObject: V1VirtualMachine = getTemplateVirtualMachineObject(template);
const rootDataVolumeTemplateSpec = getRootDataVolumeTemplateSpec(vmObject);
return Boolean(rootDataVolumeTemplateSpec?.spec?.source?.registry);
};
12 changes: 12 additions & 0 deletions src/utils/resources/vm/utils/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
V1AccessCredential,
V1Bootloader,
V1CPU,
V1DataVolumeTemplateSpec,
V1Devices,
V1Disk,
V1DomainSpec,
Expand Down Expand Up @@ -79,6 +80,17 @@ export const getVolumeSnapshotStatuses = (vm: V1VirtualMachine) =>
*/
export const getDataVolumeTemplates = (vm: V1VirtualMachine) => vm?.spec?.dataVolumeTemplates || [];

/**
* A selector for the virtual machine's root data volume
* @param {V1VirtualMachine} vm the virtual machine
* @returns the virtual machine's root data volume
*/
export const getRootDataVolumeTemplateSpec = (vm: V1VirtualMachine): V1DataVolumeTemplateSpec => {
const volume = getVolumes(vm)?.find((v) => v.name === ROOTDISK);

return vm.spec.dataVolumeTemplates?.find((dv) => dv.metadata.name === volume?.dataVolume?.name);
};

/**
* A selector for the virtual machine's config maps
* @param {V1VirtualMachine} vm the virtual machine
Expand Down
3 changes: 3 additions & 0 deletions src/utils/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const getRandomChars = (len = 6): string => {
.substr(1, len);
};

export const addRandomSuffix = (str: string) => str.concat(`-${getRandomChars()}`);

export const SSH_PUBLIC_KEY_VALIDATION_REGEX =
/^(sk-)?(ssh-rsa AAAAB3NzaC1yc2|ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNT|ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzOD|ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1Mj|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|ssh-dss AAAAB3NzaC1kc3)[0-9A-Za-z+/]+[=]{0,3}( .*)?$/;

Expand Down Expand Up @@ -152,5 +154,6 @@ export const appendDockerPrefix = (image: string) => {
return image?.startsWith(DOCKER_PREFIX) ? image : DOCKER_PREFIX.concat(image);
};
export const removeDockerPrefix = (image: string) => image?.replace(DOCKER_PREFIX, '');

export const isAllNamespaces = (namespace: string) =>
!namespace || namespace === ALL_NAMESPACES || namespace === ALL_NAMESPACES_SESSION_KEY;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, {
useRef,
} from 'react';

import { useDrawerContext } from '@catalog/templatescatalog/components/TemplatesCatalogDrawer/hooks/useDrawerContext';
import { V1beta1DataVolumeSpec, V1ContainerDiskSource } from '@kubevirt-ui/kubevirt-api/kubevirt';
import CapacityInput from '@kubevirt-utils/components/CapacityInput/CapacityInput';
import { DataUpload } from '@kubevirt-utils/hooks/useCDIUpload/useCDIUpload';
Expand Down Expand Up @@ -66,6 +67,7 @@ export const SelectSource: FC<SelectSourceProps> = ({
}) => {
const { t } = useKubevirtTranslation();
const initialDiskSource = useRef(selectedSource);
const { registryCredentials, setRegistryCredentials } = useDrawerContext();

const volumeQuantity = getQuantityFromSource(selectedSource as V1beta1DataVolumeSpec);

Expand Down Expand Up @@ -182,8 +184,10 @@ export const SelectSource: FC<SelectSourceProps> = ({
{[CONTAINER_DISK_SOURCE_NAME, REGISTRY_SOURCE_NAME].includes(selectedSourceType) && (
<ContainerSource
onInputValueChange={onInputValueChange}
registryCredentials={registryCredentials}
registrySourceHelperText={registrySourceHelperText}
selectedSourceType={selectedSourceType}
setRegistryCredentials={setRegistryCredentials}
testId={testId}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import React, { FC, FormEventHandler } from 'react';
import React, { Dispatch, FC, FormEventHandler, SetStateAction } from 'react';
import { useFormContext } from 'react-hook-form';

import FormGroupHelperText from '@kubevirt-utils/components/FormGroupHelperText/FormGroupHelperText';
import { FormPasswordInput } from '@kubevirt-utils/components/FormPasswordInput/FormPasswordInput';
import { FormTextInput } from '@kubevirt-utils/components/FormTextInput/FormTextInput';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { FormGroup, ValidatedOptions } from '@patternfly/react-core';

type ContainerSourceProps = {
onInputValueChange: FormEventHandler<HTMLInputElement>;
registryCredentials: { password: string; username: string };
registrySourceHelperText: string;
selectedSourceType: string;
setRegistryCredentials: Dispatch<SetStateAction<{ password: string; username: string }>>;
testId: string;
};

const ContainerSource: FC<ContainerSourceProps> = ({
onInputValueChange,
registryCredentials,
registrySourceHelperText,
selectedSourceType,
setRegistryCredentials,
testId,
}) => {
const { t } = useKubevirtTranslation();
Expand All @@ -29,28 +34,64 @@ const ContainerSource: FC<ContainerSourceProps> = ({
? ValidatedOptions.error
: ValidatedOptions.default;

const handleCredentialsChange = (e, field: 'password' | 'username') => {
setRegistryCredentials({ ...registryCredentials, [field]: e.target.value });
};

return (
<FormGroup
className="disk-source-form-group"
fieldId={`${testId}-${selectedSourceType}`}
isRequired
label={t('Container Image')}
>
<FormTextInput
{...register(`${testId}-containerImage`, { required: true })}
aria-label={t('Container Image')}
data-test-id={`${testId}-container-source-input`}
id={`${testId}-${selectedSourceType}`}
onChange={onInputValueChange}
type="text"
validated={validated}
/>
<FormGroupHelperText validated={validated}>
{errors?.[`${testId}-containerImage`]
? t('This field is required')
: registrySourceHelperText}
</FormGroupHelperText>
</FormGroup>
<>
<FormGroup
className="disk-source-form-group"
fieldId={`${testId}-${selectedSourceType}`}
isRequired
label={t('Container Image')}
>
<FormTextInput
{...register(`${testId}-containerImage`, { required: true })}
aria-label={t('Container Image')}
data-test-id={`${testId}-container-source-input`}
id={`${testId}-${selectedSourceType}`}
onChange={onInputValueChange}
type="text"
validated={validated}
/>
<FormGroupHelperText validated={validated}>
{errors?.[`${testId}-containerImage`]
? t('This field is required')
: registrySourceHelperText}
</FormGroupHelperText>
</FormGroup>
<FormGroup
className="disk-source-form-group"
fieldId={`${testId}-${selectedSourceType}-username`}
label={t('Username')}
>
<FormTextInput
{...register(`${testId}-username`)}
aria-label={t('Username')}
data-test-id={`${testId}-container-source-username`}
id={`${testId}-${selectedSourceType}-username`}
onChange={(e) => handleCredentialsChange(e, 'username')}
type="text"
validated={validated}
/>
</FormGroup>
<FormGroup
className="disk-source-form-group"
fieldId={`${testId}-${selectedSourceType}-password`}
label={t('Password')}
>
<FormPasswordInput
{...register(`${testId}-containerImage-password`)}
aria-label={t('Password')}
data-test-id={`${testId}-container-source-password`}
id={`${testId}-${selectedSourceType}`}
onChange={(e) => handleCredentialsChange(e, 'password')}
type="text"
validated={validated}
/>
</FormGroup>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import { ROOTDISK } from '@kubevirt-utils/constants/constants';
import { CDI_BIND_REQUESTED_ANNOTATION } from '@kubevirt-utils/hooks/useCDIUpload/consts';
import { t } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { getTemplateContainerDisks } from '@kubevirt-utils/resources/template';
import { getDisks, getVolumes } from '@kubevirt-utils/resources/vm';
import { getDisks, getRootDataVolumeTemplateSpec, getVolumes } from '@kubevirt-utils/resources/vm';
import { isEmpty } from '@kubevirt-utils/utils/utils';

import { getRootDataVolume } from '../utils';

export const getRegistryHelperText = (template: V1Template) => {
const containerDisks = getTemplateContainerDisks(template);

Expand Down Expand Up @@ -68,7 +66,7 @@ export const overrideVirtualMachineDataVolumeSpec = (
customSource?: V1beta1DataVolumeSpec,
): V1VirtualMachine => {
return produceVMDisks(virtualMachine, (draftVM) => {
const rootDataVolume = getRootDataVolume(draftVM);
const rootDataVolume = getRootDataVolumeTemplateSpec(draftVM);

if (isEmpty(customSource)) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ export const initialValue: DrawerContext = {
diskFile: null,
diskUpload: null,
isBootSourceAvailable: null,
registryCredentials: { password: '', username: '' },
setCDFile: null,
setDiskFile: null,
setRegistryCredentials: null,
setSSHDetails: null,
setStorageClassName: null,
setTemplate: null,
Expand Down
Loading
Loading