Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const DroppableFileInputField: React.FC<DroppableFileInputFieldProps> = ({
onChange && onChange(fileData);
}}
inputFileData={field.value}
inputFieldHelpText={helpText}
filenamePlaceholder={helpText}
aria-describedby={helpText ? `${fieldId}-helper` : undefined}
/>
</FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface FieldProps {

export interface DroppableFileInputFieldProps extends FieldProps {
onChange?: (fileData: string) => void;
helpText?: string;
label?: string;
}
export interface BaseInputFieldProps extends FieldProps {
type?: TextInputTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,62 @@ describe('Create key/value secrets', () => {
});
});
});

it('Validate editing text field does not corrupt binary data (OCPBUGS-70273)', () => {
const mixedSecretName = `key-value-mixed-secret-${testName}`;
const textKey = 'textfield';
const textValue = 'original-password';
const updatedTextValue = 'updated-password';
const binaryKey = 'binaryfield';

// Create a secret with both text and binary data using CLI
cy.exec(
`oc create secret generic ${mixedSecretName} -n ${testName} --from-literal=${textKey}=${textValue} --from-file=${binaryKey}=${Cypress.config(
'fileServerFolder',
)}/fixtures/${binaryFilename}`,
);

// Capture the original binary data
cy.exec(
`oc get secret -n ${testName} ${mixedSecretName} --template '{{.data.${binaryKey}}}'`,
).then((originalBinary) => {
// Edit the secret via the console
cy.visit(`/k8s/ns/${testName}/secrets/${mixedSecretName}`);
detailsPage.isLoaded();
detailsPage.clickPageActionFromDropdown('Edit Secret');

// Modify only the text field
cy.byTestID('secret-key')
.should('have.length', 2)
.each(($el) => {
if ($el.val() === textKey) {
// Find the corresponding value textarea and update it
cy.byLegacyTestID('file-input-textarea').first().clear().type(updatedTextValue);
}
});

// Verify binary field shows the binary alert (indicates it's still treated as binary)
cy.byTestID('file-input-binary-alert').should('exist');

secrets.save();
cy.byTestID('loading-indicator').should('not.exist');
detailsPage.isLoaded();

// Verify the text field was updated
secrets.clickRevealValues();
cy.byTestID('copy-to-clipboard').should('contain.text', updatedTextValue);

// Verify the binary data was NOT corrupted
cy.exec(
`oc get secret -n ${testName} ${mixedSecretName} --template '{{.data.${binaryKey}}}'`,
).then((updatedBinary) => {
expect(updatedBinary.stdout).to.equal(originalBinary.stdout);
});

// Cleanup
cy.exec(`oc delete secret -n ${testName} ${mixedSecretName}`, {
failOnNonZeroExit: false,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,19 @@ export const verifyIDPFileFields = async ({
const input = await screen.findByLabelText(`${inputLabel} filename`);
verifyFormElementBasics(input, 'text', '');

// Verify browse button visible and input element 'type' attribute within its container
// Verify browse button visible and find the hidden file input within its container
const browseContainer = screen.getByTestId(`${idPrefix}-file`);
expect(browseContainer).toBeInTheDocument();
const fileInput = within(browseContainer).getByLabelText('Browse...') as HTMLInputElement;
verifyFormElementBasics(fileInput, 'file');

// Verify the browse button is visible
const browseButton = within(browseContainer).getByRole('button', { name: 'Browse...' });
expect(browseButton).toBeVisible();

// The new PatternFly FileUpload component hides the actual file input
// We need to query by type since it's hidden and has no accessible role/label
const fileInput = browseContainer.querySelector('input[type="file"]') as HTMLInputElement;
expect(fileInput).toBeTruthy();
expect(fileInput.type).toBe('file');

const contentElement = screen.getByLabelText(inputLabel);
verifyFormElementBasics(contentElement, '', '', isRequired);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ export const AddBasicAuthPage: React.FC = () => {
inputFileData={certFileContent}
id="cert-file-input"
label={t('public~Certificate')}
hideContents
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~PEM-encoded TLS client certificate to present when connecting to the server.',
)}
/>
Expand All @@ -222,8 +221,7 @@ export const AddBasicAuthPage: React.FC = () => {
inputFileData={keyFileContent}
id="key-file-input"
label={t('public~Key')}
hideContents
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~PEM-encoded TLS private key for the client certificate. Required if certificate is specified.',
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,10 @@ export const AddHTPasswdPage = () => {
inputFileData={htpasswdFileContent}
id="htpasswd-file"
label={t('public~HTPasswd file')}
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~Upload an HTPasswd file created using the htpasswd command.',
)}
isRequired
hideContents
/>
</div>
<ButtonBar errorMessage={errorMessage} inProgress={inProgress}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export const IDPCAFileInput: React.FC<IDPCAFileInputProps> = ({
id={id}
label={t('public~CA file')}
isRequired={isRequired}
hideContents
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,7 @@ export const AddKeystonePage = () => {
inputFileData={certFileContent}
id="cert-file-input"
label={t('public~Certificate')}
hideContents
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~PEM-encoded TLS client certificate to present when connecting to the server.',
)}
/>
Expand All @@ -239,8 +238,7 @@ export const AddKeystonePage = () => {
inputFileData={keyFileContent}
id="key-file-input"
label={t('public~Key')}
hideContents
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~PEM-encoded TLS private key for the client certificate. Required if certificate is specified.',
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export const OpaqueSecretFormEntry: FCC<OpaqueSecretFormEntryProps> = ({
</FormGroup>
<DroppableFileInput
onChange={handleValueChange}
inputFileData={Base64.decode(entry.value)}
inputFileData={entry.isBinary_ ? entry.value : Base64.decode(entry.value)}
isBase64Input={entry.isBinary_}
id={`${key}-value`}
label={t('public~Value')}
inputFieldHelpText={t(
textareaFieldHelpText={t(
'public~Drag and drop file with your value here or browse to upload it.',
)}
inputFileIsBinary={entry.isBinary_}
/>
</FormFieldGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const PullSecretUploadForm: FCC<PullSecretUploadFormProps> = ({
inputFileData={configFile}
id="docker-config"
label={t('public~Configuration file')}
inputFieldHelpText={t('public~Upload a .dockercfg or .docker/config.json file.')}
textareaFieldHelpText={t(
'public~File with credentials and other configuration for connecting to a secured image registry.',
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ export const SSHAuthSubform: FCC<SSHAuthSubformProps> = ({ onChange, stringData
inputFileData={stringData['ssh-privatekey'] || ''}
id="ssh-privatekey"
label={t('public~SSH private key')}
inputFieldHelpText={t(
'public~Drag and drop file with your private SSH key here or browse to upload it.',
)}
textareaFieldHelpText={t('public~Private SSH key file for Git authentication.')}
isRequired={true}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as _ from 'lodash-es';
import { FCC, useState, FormEvent } from 'react';
import * as _ from 'lodash';
import { FCC, useState, useMemo, FormEvent } from 'react';
import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle';
import { useTranslation } from 'react-i18next';
import { Base64 } from 'js-base64';
Expand Down Expand Up @@ -66,6 +66,17 @@ export const SecretFormWrapper: FCC<BaseEditSecretProps_> = (props) => {
return acc;
}, {}),
);
// Store binary data separately to preserve it during edits
const binaryData = useMemo(
() =>
Object.entries(props.obj?.data ?? {}).reduce<Record<string, string>>((acc, [key, value]) => {
if (isBinary(null, Buffer.from(value, 'base64'))) {
acc[key] = value;
}
return acc;
}, {}),
[props.obj?.data],
);
const [base64StringData, setBase64StringData] = useState(props?.obj?.data ?? {});
const [disableForm, setDisableForm] = useState(false);
const title = useSecretTitle(isCreate, formType);
Expand All @@ -74,7 +85,20 @@ export const SecretFormWrapper: FCC<BaseEditSecretProps_> = (props) => {

const onDataChanged = (secretsData) => {
setStringData({ ...secretsData?.stringData });
setBase64StringData({ ...secretsData?.base64StringData });
// Preserve binary values by merging them with form data
// Only backfill missing keys from binaryData, don't overwrite edited entries
const mergedData = Object.entries(binaryData).reduce(
(acc, [key, value]) => {
// Only add binary entry if it's missing from form data
if (acc[key] === undefined) {
acc[key] = value;
}
// Otherwise keep the existing value from form data
return acc;
},
{ ...secretsData?.base64StringData },
);
setBase64StringData(mergedData);
};

const onError = (err) => {
Expand Down
Loading