From 0064ee3b0c72be89589287f354861920b0645577 Mon Sep 17 00:00:00 2001 From: YanJin Date: Thu, 4 Jul 2024 16:16:18 +0200 Subject: [PATCH 1/3] ARTESCA-3019: Add oracle cloud object storage support --- .../LocationDetails/LocationDetailsOracle.tsx | 190 ++++++++++++++++++ .../__tests__/LocationDetailsOracle.test.tsx | 47 +++++ .../LocationDetails/storageOptions.ts | 11 + src/react/locations/utils.tsx | 4 +- src/types/config.ts | 4 +- 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/react/locations/LocationDetails/LocationDetailsOracle.tsx create mode 100644 src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx diff --git a/src/react/locations/LocationDetails/LocationDetailsOracle.tsx b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx new file mode 100644 index 000000000..d7a036847 --- /dev/null +++ b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx @@ -0,0 +1,190 @@ +import { Input } from '@scality/core-ui/dist/components/inputv2/inputv2'; +import React, { useEffect, useState } from 'react'; +import { LocationDetailsFormProps } from '.'; +import { + FormGroup, + FormSection, +} from '@scality/core-ui/dist/components/form/Form.component'; +import { Checkbox } from '@scality/core-ui/dist/components/checkbox/Checkbox.component'; + +type State = { + bucketMatch: boolean; + accessKey: string; + secretKey: string; + bucketName: string; + namespace: string; + region: string; + endpoint: string; +}; +const INIT_STATE: State = { + bucketMatch: false, + accessKey: '', + secretKey: '', + bucketName: '', + namespace: '', + region: '', + endpoint: '', +}; + +export const oracleCloudEndpointBuilder = (namespace: string, region: string) => + `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`; + +export default function LocationDetailsOracle({ + details, + editingExisting, + onChange, +}: LocationDetailsFormProps) { + const [formState, setFormState] = useState(() => { + return { + ...Object.assign({}, INIT_STATE, details, { secretKey: '' }), + }; + }); + const onFormItemChange = (e: React.ChangeEvent) => { + const target = e.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + if (target.name === 'namespace' || target.name === 'region') { + setFormState({ + ...formState, + [target.name]: value, + endpoint: oracleCloudEndpointBuilder( + target.name === 'namespace' ? (value as string) : formState.namespace, + target.name === 'region' ? (value as string) : formState.region, + ), + }); + } else { + setFormState({ ...formState, [target.name]: value }); + } + + if (onChange) { + //remove the namespace and region from the formState + //as it is not part of the LocationS3Details + const { namespace, region, ...rest } = formState; + onChange({ ...rest, [target.name]: value }); + } + }; + + //TODO check why the tests expect onChange to be called on mount + useEffect(() => { + const { namespace, region, ...rest } = formState; + onChange({ ...rest, endpoint: formState.endpoint }); + }, []); + + return ( + <> + + + } + required + labelHelpTooltip="The namespace of the object storage." + helpErrorPosition="bottom" + /> + + } + required + labelHelpTooltip="The region of the object storage." + helpErrorPosition="bottom" + /> + + } + required + label="Access Key" + helpErrorPosition="bottom" + /> + + + } + /> + + } + helpErrorPosition="bottom" + /> + + + + } + helpErrorPosition="bottom" + help="Your objects will be stored in the target bucket without a source-bucket prefix." + error={ + formState.bucketMatch + ? 'Storing multiple buckets in a location with this option enabled can lead to data loss.' + : undefined + } + /> + + + ); +} diff --git a/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx new file mode 100644 index 000000000..a27907121 --- /dev/null +++ b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx @@ -0,0 +1,47 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Wrapper } from '../../../utils/testUtil'; +import LocationDetailsOracle from '../LocationDetailsOracle'; +import { ORACLE_CLOUD_LOCATION_KEY } from '../../../../types/config'; + +const selectors = { + namespaceSelector: () => screen.getByLabelText(/Namespace/), + regionSelector: () => screen.getByLabelText(/Region/), + targetBucketSelector: () => screen.getByLabelText(/Target Bucket Name/), + accessKeySelector: () => screen.getByLabelText(/Access Key/), + secretKeySelector: () => screen.getByLabelText(/Secret Key/), +}; +const props = { + details: {}, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, +}; +const namespace = 'namespace'; +const region = 'eu-paris-1'; +describe('class ', () => { + it('should call onChange on mount', async () => { + //S + let location = {}; + render( + //@ts-ignore + (location = l)} />, + { wrapper: Wrapper }, + ); + waitFor(() => { + expect(selectors.namespaceSelector()).toBeInTheDocument(); + }); + //E + await userEvent.type(selectors.namespaceSelector(), namespace); + await userEvent.type(selectors.regionSelector(), region); + await userEvent.type(selectors.targetBucketSelector(), 'target-bucket'); + await userEvent.type(selectors.accessKeySelector(), 'accessKey'); + await userEvent.type(selectors.secretKeySelector(), 'secretKey'); + expect(location).toEqual({ + bucketMatch: false, + endpoint: `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`, + bucketName: 'target-bucket', + accessKey: 'accessKey', + secretKey: 'secretKey', + }); + }); +}); diff --git a/src/react/locations/LocationDetails/storageOptions.ts b/src/react/locations/LocationDetails/storageOptions.ts index c9a203b9f..7903ec64b 100644 --- a/src/react/locations/LocationDetails/storageOptions.ts +++ b/src/react/locations/LocationDetails/storageOptions.ts @@ -13,6 +13,7 @@ import LocationDetailsDOSpaces from './LocationDetailsDOSpaces'; import LocationDetailsGcp from './LocationDetailsGcp'; import LocationDetailsHyperdriveV2 from './LocationDetailsHyperdriveV2'; import LocationDetailsNFS from './LocationDetailsNFS'; +import LocationDetailsOracle from './LocationDetailsOracle'; import LocationDetailsSproxyd from './LocationDetailsSproxyd'; import LocationDetailsTapeDMF from './LocationDetailsTapeDMF'; import LocationDetailsWasabi from './LocationDetailsWasabi'; @@ -213,4 +214,14 @@ export const storageOptions: Record = { supportsReplicationSource: true, hasIcon: false, }, + 'location-oracle-ring-s3-v1': { + name: 'Oracle Cloud Object Storage', + short: 'Oracle', + formDetails: LocationDetailsOracle, + supportsVersioning: true, + supportsReplicationTarget: true, + supportsReplicationSource: true, + hasIcon: false, + checkCapability: 'locationTypeS3Custom', + }, }; diff --git a/src/react/locations/utils.tsx b/src/react/locations/utils.tsx index 60278e6db..48d73405d 100644 --- a/src/react/locations/utils.tsx +++ b/src/react/locations/utils.tsx @@ -3,6 +3,7 @@ import { JAGUAR_S3_LOCATION_KEY, Location as LegacyLocation, LocationTypeKey, + ORACLE_CLOUD_LOCATION_KEY, ORANGE_S3_LOCATION_KEY, OUTSCALE_PUBLIC_S3_LOCATION_KEY, OUTSCALE_SNC_S3_LOCATION_KEY, @@ -174,7 +175,8 @@ export const checkIsRingS3Reseller = (locationType: LocationTypeKey) => { locationType === JAGUAR_S3_LOCATION_KEY || locationType === ORANGE_S3_LOCATION_KEY || locationType === OUTSCALE_PUBLIC_S3_LOCATION_KEY || - locationType === OUTSCALE_SNC_S3_LOCATION_KEY + locationType === OUTSCALE_SNC_S3_LOCATION_KEY || + locationType === ORACLE_CLOUD_LOCATION_KEY ); }; diff --git a/src/types/config.ts b/src/types/config.ts index 3424ed3f8..5b556346a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -17,6 +17,7 @@ export const OUTSCALE_SNC_S3_ENDPOINT = 'https://oos.cloudgouv-eu-west-1.outscale.com'; export const OUTSCALE_SNC_S3_LOCATION_KEY = 'location-3ds-outscale-oos-snc'; +export const ORACLE_CLOUD_LOCATION_KEY = 'location-oracle-ring-s3-v1'; export type LocationName = string; type LocationS3Type = @@ -26,7 +27,8 @@ type LocationS3Type = | 'location-orange-ring-s3-v1' | 'location-aws-s3-v1' | 'location-3ds-outscale-oos-public' - | 'location-3ds-outscale-oos-snc'; + | 'location-3ds-outscale-oos-snc' + | 'location-oracle-ring-s3-v1'; type LocationFSType = | 'location-scality-hdclient-v2' | 'location-aws-s3-v1' From aa4bfc8ef3db7471fe3a949fb1464e5be7bc31be Mon Sep 17 00:00:00 2001 From: YanJin Date: Fri, 5 Jul 2024 10:52:48 +0200 Subject: [PATCH 2/3] ARTESCA-3019: Handle the case of edit oracle location --- .../LocationDetails/LocationDetailsOracle.tsx | 16 +++++- .../__tests__/LocationDetailsOracle.test.tsx | 56 ++++++++++++++----- src/react/utils/storageOptions.ts | 7 ++- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/react/locations/LocationDetails/LocationDetailsOracle.tsx b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx index d7a036847..0c98c632e 100644 --- a/src/react/locations/LocationDetails/LocationDetailsOracle.tsx +++ b/src/react/locations/LocationDetails/LocationDetailsOracle.tsx @@ -29,6 +29,17 @@ const INIT_STATE: State = { export const oracleCloudEndpointBuilder = (namespace: string, region: string) => `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`; +export const getNamespaceAndRegion = (endpoint: string) => { + if (!endpoint) return { namespace: '', region: '' }; + const regex = + /https:\/\/(?.+)\.compat\.objectstorage\.(?.+).oraclecloud.com/; + const parts = endpoint.match(regex); + return { + namespace: parts.groups['namespace'], + region: parts.groups['region'], + }; +}; + export default function LocationDetailsOracle({ details, editingExisting, @@ -36,7 +47,10 @@ export default function LocationDetailsOracle({ }: LocationDetailsFormProps) { const [formState, setFormState] = useState(() => { return { - ...Object.assign({}, INIT_STATE, details, { secretKey: '' }), + ...Object.assign({}, INIT_STATE, details, { + secretKey: '', + ...getNamespaceAndRegion(details.endpoint), + }), }; }); const onFormItemChange = (e: React.ChangeEvent) => { diff --git a/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx index a27907121..49f6f23f4 100644 --- a/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx +++ b/src/react/locations/LocationDetails/__tests__/LocationDetailsOracle.test.tsx @@ -11,37 +11,65 @@ const selectors = { accessKeySelector: () => screen.getByLabelText(/Access Key/), secretKeySelector: () => screen.getByLabelText(/Secret Key/), }; -const props = { - details: {}, - onChange: () => {}, - locationType: ORACLE_CLOUD_LOCATION_KEY, -}; + const namespace = 'namespace'; const region = 'eu-paris-1'; -describe('class ', () => { - it('should call onChange on mount', async () => { +const targetBucketName = 'target-bucket'; +const accessKey = 'accessKey'; +const secretKey = 'secretKey'; + +describe('LocationDetailsOracle', () => { + it('should call onChange with the expected props', async () => { //S + const props = { + details: {}, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, + }; let location = {}; render( //@ts-ignore (location = l)} />, { wrapper: Wrapper }, ); - waitFor(() => { + await waitFor(() => { expect(selectors.namespaceSelector()).toBeInTheDocument(); }); //E await userEvent.type(selectors.namespaceSelector(), namespace); await userEvent.type(selectors.regionSelector(), region); - await userEvent.type(selectors.targetBucketSelector(), 'target-bucket'); - await userEvent.type(selectors.accessKeySelector(), 'accessKey'); - await userEvent.type(selectors.secretKeySelector(), 'secretKey'); + await userEvent.type(selectors.targetBucketSelector(), targetBucketName); + await userEvent.type(selectors.accessKeySelector(), accessKey); + await userEvent.type(selectors.secretKeySelector(), secretKey); expect(location).toEqual({ bucketMatch: false, endpoint: `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`, - bucketName: 'target-bucket', - accessKey: 'accessKey', - secretKey: 'secretKey', + bucketName: targetBucketName, + accessKey: accessKey, + secretKey: secretKey, + }); + }); + it('should render the namespace and region while editing', async () => { + //S + const editProps = { + details: { + endpoint: `https://${namespace}.compat.objectstorage.${region}.oraclecloud.com`, + bucketName: targetBucketName, + accessKey: accessKey, + secretKey: secretKey, + }, + onChange: () => {}, + locationType: ORACLE_CLOUD_LOCATION_KEY, + }; + render( + //@ts-ignore + (location = l)} />, + { wrapper: Wrapper }, + ); + //V + await waitFor(() => { + expect(selectors.namespaceSelector()).toHaveValue(namespace); }); + expect(selectors.regionSelector()).toHaveValue(region); }); }); diff --git a/src/react/utils/storageOptions.ts b/src/react/utils/storageOptions.ts index 81457db5c..e28cd2ebe 100644 --- a/src/react/utils/storageOptions.ts +++ b/src/react/utils/storageOptions.ts @@ -8,6 +8,7 @@ import { JAGUAR_S3_ENDPOINT, JAGUAR_S3_LOCATION_KEY, Location as LegacyLocation, + ORACLE_CLOUD_LOCATION_KEY, ORANGE_S3_ENDPOINT, ORANGE_S3_LOCATION_KEY, OUTSCALE_PUBLIC_S3_ENDPOINT, @@ -34,8 +35,8 @@ export function checkIfExternalLocation(locations: LocationInfo[]): boolean { /** * Retrieve the `LocationTypeKey` so that it can be use to to get the right * storage option. - * The `JAGUAR_S3_LOCATION_KEY` and `ORANGE_S3_LOCATION_KEY` work like - * `location-scality-ring-s3-v1` in the UI with predefine values but are not + * The `JAGUAR_S3_LOCATION_KEY`,`ORANGE_S3_LOCATION_KEY` and `ORACLE_CLOUD_LOCATION_KEY` + * work like `location-scality-ring-s3-v1` in the UI with predefine values but are not * implemented in the backend. * * We need to add extra logic because changing the backend is expensive. @@ -65,6 +66,8 @@ export const getLocationTypeKey = ( return OUTSCALE_PUBLIC_S3_LOCATION_KEY; } else if (location.details.endpoint === OUTSCALE_SNC_S3_ENDPOINT) { return OUTSCALE_SNC_S3_LOCATION_KEY; + } else if (location.details.endpoint.endsWith('oraclecloud.com')) { + return ORACLE_CLOUD_LOCATION_KEY; } else { return 'locationType' in location ? location.locationType From 05158bc2bd12c2c7eaddfa34d665fef1d78db7e0 Mon Sep 17 00:00:00 2001 From: YanJin Date: Fri, 5 Jul 2024 11:07:23 +0200 Subject: [PATCH 3/3] increase the timeout for locationlist --- src/react/locations/__tests__/LocationList.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/locations/__tests__/LocationList.test.tsx b/src/react/locations/__tests__/LocationList.test.tsx index 27ed87e32..a10e10cd5 100644 --- a/src/react/locations/__tests__/LocationList.test.tsx +++ b/src/react/locations/__tests__/LocationList.test.tsx @@ -33,7 +33,7 @@ const server = setupServer( describe('LocationList', () => { beforeAll(() => { - jest.setTimeout(30_000); + jest.setTimeout(50_000); mockOffsetSize(500, 100); server.listen({ onUnhandledRequest: 'error' }); });