diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 6272b3090..693b7f21e 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -628,10 +628,23 @@ "Data Foundation will use a StorageClass provided by the Local Storage Operator (LSO) on top of your attached drives. This option is available on any platform with devices attached to nodes.": "Data Foundation will use a StorageClass provided by the Local Storage Operator (LSO) on top of your attached drives. This option is available on any platform with devices attached to nodes.", "Connect an external storage platform": "Connect an external storage platform", "Data Foundation will create a dedicated StorageClass.": "Data Foundation will create a dedicated StorageClass.", + "Use external PostgreSQL": "Use external PostgreSQL", + "Allow Noobaa to connect to an external postgres server": "Allow Noobaa to connect to an external postgres server", "NFS is currently not supported for external storage type. To proceed with an external storage type, disable this option.": "NFS is currently not supported for external storage type. To proceed with an external storage type, disable this option.", "NFS is currently not supported for external storage type.": "NFS is currently not supported for external storage type.", "Enable network file system (NFS)": "Enable network file system (NFS)", "Allow NFS to use low resources by default.": "Allow NFS to use low resources by default.", + "Server name": "Server name", + "Port": "Port", + "Database name": "Database name", + "Enable TLS/SSL": "Enable TLS/SSL", + "Enable this for encrytion on flight with the Postgres server": "Enable this for encrytion on flight with the Postgres server", + "Allow self-signed certificates": "Allow self-signed certificates", + "Adding this option will allow the postgres server to use a self-signed certificates.": "Adding this option will allow the postgres server to use a self-signed certificates.", + "Enable client-side certificates": "Enable client-side certificates", + "Select this option to upload and use your client side certificates": "Select this option to upload and use your client side certificates", + "Accepted file types: .pem, .crt, .key": "Accepted file types: .pem, .crt, .key", + "Files should be named private and public followed by compatible extensions": "Files should be named private and public followed by compatible extensions", "Deploys MultiCloud Object Gateway without block and file services.": "Deploys MultiCloud Object Gateway without block and file services.", "Deploys Data Foundation with block, shared fileSystem and object services.": "Deploys Data Foundation with block, shared fileSystem and object services.", "Deployment type": "Deployment type", @@ -795,7 +808,6 @@ "Please enter a URL": "Please enter a URL", "Please enter a valid port": "Please enter a valid port", "Address": "Address", - "Port": "Port", "Client certificate": "Client certificate", "CA certificate": "CA certificate", "Client private key": "Client private key", @@ -1115,6 +1127,7 @@ "Subscription": "Subscription", "Subscriptions": "Subscriptions", "Project": "Project", + "Show password": "Show password", "Deployment details": "Deployment details", "Metrics": "Metrics", "CPU usage": "CPU usage", diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.scss b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.scss index 1e4eb88a0..d8d718fbf 100644 --- a/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.scss +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.scss @@ -18,3 +18,15 @@ .odf-backing-store__dropdown--margin-top { margin-top: var(--pf-global--spacer--sm); } + +.odf-backing-store__ssl { + padding-left: var(--pf-global--spacer--xl); + padding-bottom: var(--pf-global--spacer--xl); ; +} + +.odf-backing-store__tls--margin-top { + margin-top: var(--pf-global--spacer--md); +} + + + diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.tsx b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.tsx index 2c46d1dd5..6053d9873 100644 --- a/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.tsx +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/backing-storage-step.tsx @@ -35,10 +35,12 @@ import { Radio, Alert, AlertVariant, + Checkbox, } from '@patternfly/react-core'; import { ErrorHandler } from '../../error-handler'; import { WizardState, WizardDispatch } from '../../reducer'; import { EnableNFS } from './enable-nfs'; +import { PostgresConnectionDetails } from './noobaa-external-postgres/postgres-connection-details'; import { SelectDeployment } from './select-deployment'; import { SetCephRBDStorageClassDefault } from './set-rbd-sc-default'; import './backing-storage-step.scss'; @@ -189,6 +191,8 @@ export const BackingStorage: React.FC = ({ isRBDStorageClassDefault, externalStorage, deployment, + externalPostgres, + useExternalPostgres, } = state; const { t } = useCustomTranslation(); @@ -364,6 +368,38 @@ export const BackingStorage: React.FC = ({ /> )} + + dispatch({ + type: 'backingStorage/useExternalPostgres', + payload: !useExternalPostgres, + }) + } + className="odf-backing-store__radio--margin-bottom" + /> + {useExternalPostgres && ( + + )} ); diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/noobaa-external-postgres/postgres-connection-details.tsx b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/noobaa-external-postgres/postgres-connection-details.tsx new file mode 100644 index 000000000..7bdc2c341 --- /dev/null +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/backing-storage-step/noobaa-external-postgres/postgres-connection-details.tsx @@ -0,0 +1,288 @@ +import * as React from 'react'; +import { UploadFile } from '@odf/shared/file-handling'; +import { PasswordInput } from '@odf/shared/text-inputs'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { + FormGroup, + Checkbox, + TextInput, + Form, + GridItem, + Grid, + Text, + TextVariants, + Stack, +} from '@patternfly/react-core'; +import { WizardDispatch, WizardState } from '../../../reducer'; +import '../backing-storage-step.scss'; + +const updatePrivateTlsFile = (dispatch: WizardDispatch, file: File) => { + dispatch({ + type: 'backingStorage/externalPostgres/tls/keys/setPrivateKey', + payload: file, + }); +}; + +const updatePublicTlsFile = (dispatch: WizardDispatch, file: File) => { + dispatch({ + type: 'backingStorage/externalPostgres/tls/keys/setPublicKey', + payload: file, + }); +}; + +export const PostgresConnectionDetails: React.FC = + ({ + dispatch, + username, + password, + serverName, + port, + databaseName, + tlsEnabled, + allowSelfSignedCerts, + enableClientSideCerts, + tlsFiles, + }) => { + const { t } = useCustomTranslation(); + + const processKeys = React.useCallback( + (keyFiles: File[]) => { + if (keyFiles.some((file) => file.name.startsWith('private'))) { + updatePrivateTlsFile( + dispatch, + keyFiles.find((file) => file.name.startsWith('private')) + ); + } else { + updatePrivateTlsFile(dispatch, null); + } + + if (keyFiles.some((file) => file.name.startsWith('public'))) { + updatePublicTlsFile( + dispatch, + keyFiles.find((file) => file.name.startsWith('public')) + ); + } else { + updatePublicTlsFile(dispatch, null); + } + }, + [dispatch] + ); + + return ( +
+ Connection details + + + + { + dispatch({ + type: 'backingStorage/externalPostgres/setUsername', + payload: newUsername, + }); + }} + isRequired + /> + + + + + + { + dispatch({ + type: 'backingStorage/externalPostgres/setPassword', + payload: newPassword, + }); + }} + isRequired + /> + + + + + + { + dispatch({ + type: 'backingStorage/externalPostgres/setServerName', + payload: newServer, + }); + }} + isRequired + /> + + + + + { + dispatch({ + type: 'backingStorage/externalPostgres/setPort', + payload: newPort, + }); + }} + isRequired + /> + + + + + + { + dispatch({ + type: 'backingStorage/externalPostgres/setDatabaseName', + payload: newDatabase, + }); + }} + isRequired + /> + + + + + + { + const isTLSEnabled = !tlsEnabled; + dispatch({ + type: 'backingStorage/externalPostgres/tls/enableTLS', + payload: !tlsEnabled, + }); + + dispatch({ + type: 'backingStorage/externalPostgres/tls/allowSelfSignedCerts', + payload: + !isTLSEnabled && allowSelfSignedCerts + ? false + : allowSelfSignedCerts, + }); + + dispatch({ + type: 'backingStorage/externalPostgres/tls/enableClientSideCerts', + payload: + !isTLSEnabled && enableClientSideCerts + ? false + : enableClientSideCerts, + }); + }} + className="odf-backing-store__radio--margin-bottom" + /> + {tlsEnabled && ( + + + dispatch({ + type: 'backingStorage/externalPostgres/tls/allowSelfSignedCerts', + payload: !allowSelfSignedCerts, + }) + } + /> + { + dispatch({ + type: 'backingStorage/externalPostgres/tls/enableClientSideCerts', + payload: !enableClientSideCerts, + }); + + dispatch({ + type: 'backingStorage/externalPostgres/tls/keys/setPrivateKey', + payload: null, + }); + + dispatch({ + type: 'backingStorage/externalPostgres/tls/keys/setPublicKey', + payload: null, + }); + }} + /> + + )} + {enableClientSideCerts && ( + + file.name.startsWith('private') || + file.name.startsWith('public') + } + /> + )} + + + +
+ ); + }; + +type PostgresConnectionDetailsProps = { + dispatch: WizardDispatch; + username: WizardState['backingStorage']['externalPostgres']['username']; + password: WizardState['backingStorage']['externalPostgres']['password']; + serverName: WizardState['backingStorage']['externalPostgres']['serverName']; + port: WizardState['backingStorage']['externalPostgres']['port']; + databaseName: WizardState['backingStorage']['externalPostgres']['databaseName']; + tlsEnabled: WizardState['backingStorage']['externalPostgres']['tls']['enabled']; + allowSelfSignedCerts: WizardState['backingStorage']['externalPostgres']['tls']['allowSelfSignedCerts']; + enableClientSideCerts: WizardState['backingStorage']['externalPostgres']['tls']['enableClientSideCerts']; + tlsFiles: File[]; +}; diff --git a/packages/odf/components/create-storage-system/create-storage-system.tsx b/packages/odf/components/create-storage-system/create-storage-system.tsx index 271cec34e..971636490 100644 --- a/packages/odf/components/create-storage-system/create-storage-system.tsx +++ b/packages/odf/components/create-storage-system/create-storage-system.tsx @@ -96,6 +96,24 @@ const CreateStorageSystem: React.FC = () => { ...wizardSteps, ]; + const { useExternalPostgres, externalPostgres } = state.backingStorage; + + const { + username, + password, + serverName, + port, + databaseName, + tls: { enableClientSideCerts, keys }, + } = externalPostgres; + + const hasPGEnabledButNoFields = + useExternalPostgres && + (!username || !password || !serverName || !port || !databaseName); + + const hasClientCertsEnabledButNoKeys = + enableClientSideCerts && (!keys.private || !keys.public); + return ( <> @@ -108,7 +126,12 @@ const CreateStorageSystem: React.FC = () => { hasOCS={hasOCS} dispatch={dispatch} disableNext={ - !ssLoaded || !!ssLoadError || !infraLoaded || !!infraLoadError + !ssLoaded || + !!ssLoadError || + !infraLoaded || + !!infraLoadError || + hasPGEnabledButNoFields || + hasClientCertsEnabledButNoKeys } history={history} supportedExternalStorage={supportedExternalStorage} diff --git a/packages/odf/components/create-storage-system/footer.tsx b/packages/odf/components/create-storage-system/footer.tsx index 897fdd87b..485613274 100644 --- a/packages/odf/components/create-storage-system/footer.tsx +++ b/packages/odf/components/create-storage-system/footer.tsx @@ -27,6 +27,7 @@ import { createClusterKmsResources } from '../kms-config/utils'; import { getExternalStorage } from '../utils'; import { createExternalSubSystem, + createNoobaaExternalPostgresResources, createStorageCluster, createStorageSystem, labelNodes, @@ -136,39 +137,60 @@ const handleReviewAndCreateNext = async ( nodes, capacityAndNodes, } = state; - const { externalStorage, deployment, type } = state.backingStorage; + const { + externalStorage, + deployment, + type, + useExternalPostgres, + externalPostgres, + } = state.backingStorage; const inTransitChecked = state.securityAndNetwork.encryption.inTransit; const { encryption, kms } = state.securityAndNetwork; const isRhcs: boolean = externalStorage === OCSStorageClusterModel.kind; const isMCG: boolean = deployment === DeploymentType.MCG; + const createAdditionalFeatureResources = async () => { + if (capacityAndNodes.enableTaint && !isMCG) await taintNodes(nodes); + + if (encryption.advanced) + await Promise.all( + createClusterKmsResources( + kms.providerState, + odfNamespace, + kms.provider, + isMCG + ) + ); + + if (useExternalPostgres) { + let keyTexts = { private: '', public: '' }; + if (externalPostgres.tls.enableClientSideCerts) { + const keys = externalPostgres.tls.keys; + const privateKey = await keys.private.text(); + const publicKey = await keys.public.text(); + keyTexts = { private: privateKey, public: publicKey }; + } + await Promise.all( + createNoobaaExternalPostgresResources( + odfNamespace, + externalPostgres, + keyTexts + ) + ); + } + }; + try { await labelOCSNamespace(odfNamespace); if (isMCG) { - if (encryption.advanced) - await Promise.all( - createClusterKmsResources( - kms.providerState, - odfNamespace, - kms.provider, - isMCG - ) - ); + await createAdditionalFeatureResources(); await createStorageCluster(state, odfNamespace); } else if ( type === BackingStorageType.EXISTING || type === BackingStorageType.LOCAL_DEVICES ) { await labelNodes(nodes, odfNamespace); - if (capacityAndNodes.enableTaint) await taintNodes(nodes); - if (encryption.advanced) - await Promise.all( - createClusterKmsResources( - kms.providerState, - odfNamespace, - kms.provider - ) - ); + await createAdditionalFeatureResources(); await createStorageSystem( OCS_INTERNAL_CR_NAME, STORAGE_CLUSTER_SYSTEM_KIND, @@ -200,15 +222,7 @@ const handleReviewAndCreateNext = async ( await createStorageSystem(subSystemName, subSystemKind, odfNamespace); if (!hasOCS && !isRhcs) { await labelNodes(nodes, odfNamespace); - if (capacityAndNodes.enableTaint) await taintNodes(nodes); - if (encryption.advanced) - await Promise.all( - createClusterKmsResources( - kms.providerState, - odfNamespace, - kms.provider - ) - ); + await createAdditionalFeatureResources(); await createStorageCluster(state, odfNamespace); } if (!isRhcs && !!waitToCreate) await waitToCreate(model); diff --git a/packages/odf/components/create-storage-system/payloads.ts b/packages/odf/components/create-storage-system/payloads.ts index 1a993557c..cbf3ed759 100644 --- a/packages/odf/components/create-storage-system/payloads.ts +++ b/packages/odf/components/create-storage-system/payloads.ts @@ -4,6 +4,11 @@ import { } from '@odf/core/components/utils'; import { DeploymentType, BackingStorageType } from '@odf/core/types'; import { Payload } from '@odf/odf-plugin-sdk/extensions'; +import { SecretModel, getAPIVersion } from '@odf/shared'; +import { + NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME, + NOOBA_EXTERNAL_PG_SECRET_NAME, +} from '@odf/shared/constants'; import { OCSStorageClusterModel, ODFStorageSystem, @@ -11,7 +16,10 @@ import { } from '@odf/shared/models'; import { Patch, StorageSystemKind } from '@odf/shared/types'; import { getAPIVersionForModel, k8sPatchByName } from '@odf/shared/utils'; -import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; +import { + K8sResourceKind, + k8sCreate, +} from '@openshift-console/dynamic-plugin-sdk'; import { K8sKind } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; import * as _ from 'lodash-es'; import { @@ -44,6 +52,91 @@ export const createStorageSystem = async ( return k8sCreate({ model: ODFStorageSystem, data: payload }); }; +export const createSecretPayload = ( + name: string, + namespace: string, + type: string, + data?: { [key: string]: string }, + stringData?: { [key: string]: string } +): K8sResourceKind => { + const secretPayload = { + apiVersion: getAPIVersion(SecretModel), + kind: SecretModel.kind, + metadata: { + name: name, + namespace: namespace, + }, + type: type, + data: data, + stringData: stringData, + }; + + return secretPayload; +}; + +export const createNoobaaExternalPostgresResources = ( + namespace: string, + externalPostgresDetails: { + username: string; + password: string; + serverName: string; + port: string; + databaseName: string; + tls: { + allowSelfSignedCerts: boolean; + enableClientSideCerts: boolean; + }; + }, + keys?: { private: string; public: string } +): Promise[] => { + let secretResources: Promise[] = []; + const stringData = { + db_url: `postgres://${externalPostgresDetails.username}:${externalPostgresDetails.password}@${externalPostgresDetails.serverName}.namespace.svc:${externalPostgresDetails.port}/${externalPostgresDetails.databaseName}`, + }; + const noobaaExternalPostgresSecretPayload = createSecretPayload( + NOOBA_EXTERNAL_PG_SECRET_NAME, + namespace, + 'Opaque', + null, + stringData + ); + + secretResources.push( + k8sCreate({ + model: SecretModel, + data: noobaaExternalPostgresSecretPayload, + }) + ); + + if (externalPostgresDetails.tls.enableClientSideCerts) { + const privateKeyData = keys.private; + const publicKeyData = keys.public; + let data = {}; + if (privateKeyData) { + data = { ...data, 'tls.key': privateKeyData }; + } + if (publicKeyData) { + data = { ...data, 'tls.crt': publicKeyData }; + } + + const noobaaExternalPostgresTLSSecretPayload = createSecretPayload( + NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME, + namespace, + 'kubernetes.io/tls', + data + ); + + secretResources.push( + k8sCreate({ + model: SecretModel, + data: noobaaExternalPostgresTLSSecretPayload, + }) + ); + } + + return secretResources; +}; + export const createStorageCluster = async ( state: WizardState, odfNamespace: string @@ -64,8 +157,14 @@ export const createStorageCluster = async ( enableSingleReplicaPool, } = capacityAndNodes; const { encryption, publicNetwork, clusterNetwork, kms } = securityAndNetwork; - const { type, enableNFS, isRBDStorageClassDefault, deployment } = - backingStorage; + const { + type, + enableNFS, + deployment, + isRBDStorageClassDefault, + useExternalPostgres, + externalPostgres, + } = backingStorage; const { enableRDRPreparation } = dataProtection; const isNoProvisioner = storageClass?.provisioner === NO_PROVISIONER; @@ -114,7 +213,12 @@ export const createStorageCluster = async ( isSingleReplicaPoolEnabled: enableSingleReplicaPool, enableRDRPreparation, odfNamespace, + enableNoobaaClientSideCerts: externalPostgres.tls.enableClientSideCerts, + useExternalPostgres: useExternalPostgres, + allowNoobaaPostgresSelfSignedCerts: + externalPostgres.tls.allowSelfSignedCerts, }); + return k8sCreate({ model: OCSStorageClusterModel, data: payload }); }; diff --git a/packages/odf/components/create-storage-system/reducer.ts b/packages/odf/components/create-storage-system/reducer.ts index 9e66c73b6..1f1973766 100644 --- a/packages/odf/components/create-storage-system/reducer.ts +++ b/packages/odf/components/create-storage-system/reducer.ts @@ -42,6 +42,23 @@ export const initialState: CreateStorageSystemState = { isRBDStorageClassDefault: false, externalStorage: '', deployment: DeploymentType.FULL, + useExternalPostgres: false, + externalPostgres: { + username: '', + password: '', + serverName: '', + port: null, + databaseName: '', + tls: { + enabled: false, + allowSelfSignedCerts: false, + enableClientSideCerts: false, + keys: { + private: null, + public: null, + }, + }, + }, }, capacityAndNodes: { enableArbiter: false, @@ -96,6 +113,23 @@ type CreateStorageSystemState = { isRBDStorageClassDefault: boolean; externalStorage: string; deployment: DeploymentType; + useExternalPostgres: boolean; + externalPostgres: { + username: string; + password: string; + serverName: string; + port: string; + databaseName: string; + tls: { + enabled: boolean; + allowSelfSignedCerts: boolean; + enableClientSideCerts: boolean; + keys: { + private: File; + public: File; + }; + }; + }; }; createStorageClass: ExternalState; connectionDetails: ExternalCephState; @@ -267,6 +301,44 @@ export const reducer: WizardReducer = (prevState, action) => { case 'backingStorage/setIsRBDStorageClassDefault': newState.backingStorage.isRBDStorageClassDefault = action.payload; break; + case 'backingStorage/useExternalPostgres': + newState.backingStorage.useExternalPostgres = action.payload; + break; + case 'backingStorage/externalPostgres/setUsername': + newState.backingStorage.externalPostgres.username = action.payload; + break; + case 'backingStorage/externalPostgres/setPassword': + newState.backingStorage.externalPostgres.password = action.payload; + break; + case 'backingStorage/externalPostgres/setServerName': + newState.backingStorage.externalPostgres.serverName = action.payload; + break; + case 'backingStorage/externalPostgres/setPort': + newState.backingStorage.externalPostgres.port = action.payload; + break; + case 'backingStorage/externalPostgres/setDatabaseName': + newState.backingStorage.externalPostgres.databaseName = action.payload; + break; + case 'backingStorage/externalPostgres/tls/enableTLS': + newState.backingStorage.externalPostgres.tls.enabled = action.payload; + break; + case 'backingStorage/externalPostgres/tls/allowSelfSignedCerts': + newState.backingStorage.externalPostgres.tls.allowSelfSignedCerts = + action.payload; + break; + + case 'backingStorage/externalPostgres/tls/enableClientSideCerts': + newState.backingStorage.externalPostgres.tls.enableClientSideCerts = + action.payload; + break; + case 'backingStorage/externalPostgres/tls/keys/setPrivateKey': + newState.backingStorage.externalPostgres.tls.keys.private = + action.payload; + break; + case 'backingStorage/externalPostgres/tls/keys/setPublicKey': + newState.backingStorage.externalPostgres.tls.keys.public = action.payload; + break; + case 'backingStorage/setDeployment': return setDeployment(newState, action.payload); case 'backingStorage/setExternalStorage': @@ -423,4 +495,48 @@ export type CreateStorageSystemAction = | { type: 'dataProtection/enableRDRPreparation'; payload: WizardState['dataProtection']['enableRDRPreparation']; + } + | { + type: 'backingStorage/useExternalPostgres'; + payload: WizardState['backingStorage']['useExternalPostgres']; + } + | { + type: 'backingStorage/externalPostgres/setUsername'; + payload: WizardState['backingStorage']['externalPostgres']['username']; + } + | { + type: 'backingStorage/externalPostgres/setPassword'; + payload: WizardState['backingStorage']['externalPostgres']['password']; + } + | { + type: 'backingStorage/externalPostgres/setServerName'; + payload: WizardState['backingStorage']['externalPostgres']['serverName']; + } + | { + type: 'backingStorage/externalPostgres/setPort'; + payload: WizardState['backingStorage']['externalPostgres']['port']; + } + | { + type: 'backingStorage/externalPostgres/setDatabaseName'; + payload: WizardState['backingStorage']['externalPostgres']['databaseName']; + } + | { + type: 'backingStorage/externalPostgres/tls/enableTLS'; + payload: WizardState['backingStorage']['externalPostgres']['tls']['enabled']; + } + | { + type: 'backingStorage/externalPostgres/tls/allowSelfSignedCerts'; + payload: WizardState['backingStorage']['externalPostgres']['tls']['allowSelfSignedCerts']; + } + | { + type: 'backingStorage/externalPostgres/tls/enableClientSideCerts'; + payload: WizardState['backingStorage']['externalPostgres']['tls']['enableClientSideCerts']; + } + | { + type: 'backingStorage/externalPostgres/tls/keys/setPrivateKey'; + payload: WizardState['backingStorage']['externalPostgres']['tls']['keys']['private']; + } + | { + type: 'backingStorage/externalPostgres/tls/keys/setPublicKey'; + payload: WizardState['backingStorage']['externalPostgres']['tls']['keys']['public']; }; diff --git a/packages/odf/components/utils/common.ts b/packages/odf/components/utils/common.ts index 97038ae53..c6792203a 100644 --- a/packages/odf/components/utils/common.ts +++ b/packages/odf/components/utils/common.ts @@ -14,6 +14,10 @@ import { shouldDeployAsMinimal, } from '@odf/core/utils'; import { StorageClassWizardStepExtensionProps as ExternalStorage } from '@odf/odf-plugin-sdk/extensions'; +import { + NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME, + NOOBA_EXTERNAL_PG_SECRET_NAME, +} from '@odf/shared/constants'; import { getLabel, getName, @@ -368,6 +372,9 @@ type OCSRequestData = { isSingleReplicaPoolEnabled?: boolean; enableRDRPreparation?: boolean; odfNamespace: string; + useExternalPostgres?: boolean; + allowNoobaaPostgresSelfSignedCerts?: boolean; + enableNoobaaClientSideCerts?: boolean; }; export const getOCSRequestData = ({ @@ -389,6 +396,9 @@ export const getOCSRequestData = ({ isSingleReplicaPoolEnabled, enableRDRPreparation, odfNamespace, + useExternalPostgres, + allowNoobaaPostgresSelfSignedCerts, + enableNoobaaClientSideCerts, }: OCSRequestData): StorageClusterKind => { const scName: string = storageClass.name; const isNoProvisioner: boolean = storageClass?.provisioner === NO_PROVISIONER; @@ -484,6 +494,17 @@ export const getOCSRequestData = ({ }; } + if (useExternalPostgres) { + requestData.spec.multiCloudGateway.externalPGConfig.pgSecretName = + NOOBA_EXTERNAL_PG_SECRET_NAME; + requestData.spec.multiCloudGateway.externalPGConfig.allowSelfSignedCerts = + allowNoobaaPostgresSelfSignedCerts; + if (enableNoobaaClientSideCerts) { + requestData.spec.multiCloudGateway.externalPGConfig.tlsSecretName = + NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME; + } + } + return requestData; }; diff --git a/packages/shared/src/constants/common.ts b/packages/shared/src/constants/common.ts index ba4b4e2ad..d771dc767 100644 --- a/packages/shared/src/constants/common.ts +++ b/packages/shared/src/constants/common.ts @@ -8,3 +8,5 @@ export const ONE_HOUR = 60 * ONE_MINUTE; export const ALL_NAMESPACES = 'all-namespaces'; export const DEFAULT_NS = 'default'; export const RACK_LABEL = 'topology.rook.io/rack'; +export const NOOBA_EXTERNAL_PG_SECRET_NAME = 'noobaa-external-pg'; +export const NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME = 'noobaa-external-pg-tls'; diff --git a/packages/shared/src/file-handling/index.ts b/packages/shared/src/file-handling/index.ts new file mode 100644 index 000000000..b30f165df --- /dev/null +++ b/packages/shared/src/file-handling/index.ts @@ -0,0 +1 @@ +export { default as UploadFile } from './upload-file'; diff --git a/packages/shared/src/file-handling/upload-file.tsx b/packages/shared/src/file-handling/upload-file.tsx new file mode 100644 index 000000000..6be3bc925 --- /dev/null +++ b/packages/shared/src/file-handling/upload-file.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { + MultipleFileUpload, + MultipleFileUploadMain, + MultipleFileUploadStatus, + MultipleFileUploadStatusItem, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; + +type readFile = { + fileName: string; + data?: string; + loadResult?: 'danger' | 'success'; + loadError?: DOMException; +}; + +enum Status { + InProgress = 'inProgress', + Success = 'success', + Danger = 'danger', + Error = 'error', +} + +type UploadFilePickerProps = { + infoText: string; + titleText: string; + acceptedFiles: string; + uploadLimit?: number; + onFileUpload: (currentFiles: File[]) => void; + compatibleFileFilter?: (file: File) => boolean; + uploadedFiles?: File[]; +}; +const UploadFilePicker: React.FC = ({ + onFileUpload, + infoText, + titleText, + acceptedFiles, + uploadLimit, + compatibleFileFilter, + uploadedFiles = [], +}) => { + const [currentFiles, setCurrentFiles] = React.useState( + uploadedFiles.filter((file) => file != null) + ); + const [readFileData, setReadFileData] = React.useState([]); + const [showStatus, setShowStatus] = React.useState(false); + const [statusIcon, setStatusIcon] = React.useState(Status.InProgress); + + if (!showStatus && currentFiles.length > 0) { + setShowStatus(true); + } + + React.useEffect(() => { + if (readFileData.length < currentFiles.length) { + setStatusIcon(Status.InProgress); + } else if ( + readFileData.every((file) => file.loadResult === Status.Success) + ) { + setStatusIcon(Status.Success); + } else { + setStatusIcon(Status.Danger); + } + onFileUpload(currentFiles); + }, [readFileData, currentFiles, onFileUpload]); + + const removeFiles = (namesOfFilesToRemove: string[]) => { + const newCurrentFiles = currentFiles.filter( + (currentFile) => + !namesOfFilesToRemove.some((fileName) => fileName === currentFile.name) + ); + + setCurrentFiles(newCurrentFiles); + + const newReadFiles = readFileData.filter( + (readFile) => + !namesOfFilesToRemove.some((fileName) => fileName === readFile.fileName) + ); + + setReadFileData(newReadFiles); + }; + + const updateCurrentFiles = (files: File[]) => { + setCurrentFiles((prevFiles) => [...prevFiles, ...files]); + }; + + const handleFileDrop = (droppedFiles: File[]) => { + // identify what, if any, files are re-uploads of already uploaded files + const currentFileNames = currentFiles.map((file) => file.name); + const reUploads = droppedFiles.filter((droppedFile) => + currentFileNames.includes(droppedFile.name) + ); + + const compatibleFiles = droppedFiles.filter(compatibleFileFilter); + Promise.resolve() + .then(() => removeFiles(reUploads.map((file) => file.name))) + .then(() => updateCurrentFiles(compatibleFiles)); + }; + + const handleReadSuccess = (data: string, file: File) => { + setReadFileData((prevReadFiles) => [ + ...prevReadFiles, + { data, fileName: file.name, loadResult: Status.Success }, + ]); + }; + + const handleReadFail = (error: DOMException, file: File) => { + setReadFileData((prevReadFiles) => [ + ...prevReadFiles, + { loadError: error, fileName: file.name, loadResult: Status.Danger }, + ]); + }; + + const createHelperText = (file: File) => { + const fileResult = readFileData.find( + (readFile) => readFile.fileName === file.name + ); + if (fileResult?.loadError) { + return ( + + + {fileResult.loadError.toString()} + + + ); + } + }; + + return ( + <> + + + {showStatus && ( + + {currentFiles.map((file) => ( + removeFiles([file.name])} + onReadSuccess={handleReadSuccess} + onReadFail={handleReadFail} + progressHelperText={createHelperText(file)} + /> + ))} + + )} + + + ); +}; + +export default UploadFilePicker; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 265577557..5cf6ecaad 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -29,3 +29,5 @@ export * from './useCustomTranslationHook'; export * as utils from './utils'; export * from './yaml-editor'; export * from './yup-validation-resolver'; +export * from './file-handling'; +export * from './text-inputs'; diff --git a/packages/shared/src/text-inputs/index.ts b/packages/shared/src/text-inputs/index.ts new file mode 100644 index 000000000..e938c235a --- /dev/null +++ b/packages/shared/src/text-inputs/index.ts @@ -0,0 +1 @@ +export { default as PasswordInput } from './password-input'; diff --git a/packages/shared/src/text-inputs/password-input.tsx b/packages/shared/src/text-inputs/password-input.tsx new file mode 100644 index 000000000..b768c709d --- /dev/null +++ b/packages/shared/src/text-inputs/password-input.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { InputGroup, TextInput, Tooltip, Button } from '@patternfly/react-core'; +import { EyeSlashIcon, EyeIcon } from '@patternfly/react-icons'; +import { useCustomTranslation } from '../useCustomTranslationHook'; + +type PasswordInputProps = { + id: string; + value: string; + onChange: (value: string) => void; + isRequired?: boolean; +}; + +const PasswordInput: React.FC = ({ + id, + value, + onChange, + isRequired, +}) => { + const { t } = useCustomTranslation(); + const [showPassword, setShowPassword] = React.useState(false); + + return ( + + + + + + + ); +}; + +export default PasswordInput; diff --git a/packages/shared/src/types/storage.ts b/packages/shared/src/types/storage.ts index 4bc1f2f36..8d32bb235 100644 --- a/packages/shared/src/types/storage.ts +++ b/packages/shared/src/types/storage.ts @@ -38,8 +38,13 @@ export type StorageClusterKind = K8sResourceCommon & { flexibleScaling?: boolean; monDataDirHostPath?: string; multiCloudGateway?: { - reconcileStrategy: string; - dbStorageClassName: string; + reconcileStrategy?: string; + dbStorageClassName?: string; + externalPGConfig?: { + pgSecretName?: string; + allowSelfSignedCerts?: boolean; + tlsSecretName?: string; + }; }; externalStorage?: {}; };