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

ARTESCA-3019: Add oracle cloud object storage support #759

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
204 changes: 204 additions & 0 deletions src/react/locations/LocationDetails/LocationDetailsOracle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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 const getNamespaceAndRegion = (endpoint: string) => {
if (!endpoint) return { namespace: '', region: '' };
const regex =
/https:\/\/(?<namespace>.+)\.compat\.objectstorage\.(?<region>.+).oraclecloud.com/;
Dismissed Show dismissed Hide dismissed
const parts = endpoint.match(regex);
return {
namespace: parts.groups['namespace'],
region: parts.groups['region'],
};
};

export default function LocationDetailsOracle({
details,
editingExisting,
onChange,
}: LocationDetailsFormProps) {
const [formState, setFormState] = useState<State>(() => {
return {
...Object.assign({}, INIT_STATE, details, {
secretKey: '',
...getNamespaceAndRegion(details.endpoint),
}),
};
});
const onFormItemChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<>
<FormSection>
<FormGroup
id="namespace"
label="Namespace"
content={
<Input
id="namespace"
name="namespace"
type="text"
value={formState.namespace}
onChange={onFormItemChange}
placeholder="object_storage_namespace"
/>
}
required
labelHelpTooltip="The namespace of the object storage."
helpErrorPosition="bottom"
/>
<FormGroup
id="region"
label="Region"
content={
<Input
id="region"
name="region"
type="text"
value={formState.region}
onChange={onFormItemChange}
placeholder="eu-paris-1"
/>
}
required
labelHelpTooltip="The region of the object storage."
helpErrorPosition="bottom"
/>
<FormGroup
id="accessKey"
content={
<Input
name="accessKey"
id="accessKey"
type="text"
placeholder="AKI5HMPCLRB86WCKTN2C"
value={formState.accessKey}
onChange={onFormItemChange}
autoComplete="off"
/>
}
required
label="Access Key"
helpErrorPosition="bottom"
/>

<FormGroup
id="secretKey"
label="Secret Key"
required
labelHelpTooltip="Your credentials are encrypted in transit, then at rest using your instance's RSA key pair so that we're unable to see them."
helpErrorPosition="bottom"
content={
<Input
name="secretKey"
id="secretKey"
type="password"
placeholder="QFvIo6l76oe9xgCAw1N/zlPFtdTSZXMMUuANeXc6"
value={formState.secretKey}
onChange={onFormItemChange}
autoComplete="new-password"
/>
}
/>
<FormGroup
id="bucketName"
label="Target Bucket Name"
help="The Target Bucket on your location needs to have Versioning enabled."
required
content={
<Input
name="bucketName"
id="bucketName"
type="text"
placeholder="bucket-name"
value={formState.bucketName}
onChange={onFormItemChange}
autoComplete="off"
disabled={editingExisting}
/>
}
helpErrorPosition="bottom"
/>
</FormSection>
<FormSection>
<FormGroup
label=""
direction="vertical"
id="bucketMatch"
content={
<Checkbox
name="bucketMatch"
disabled={editingExisting}
checked={formState.bucketMatch}
onChange={onFormItemChange}
label={'Write objects without prefix'}
/>
}
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
}
/>
</FormSection>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 namespace = 'namespace';
const region = 'eu-paris-1';
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
<LocationDetailsOracle {...props} onChange={(l) => (location = l)} />,
{ wrapper: Wrapper },
);
await waitFor(() => {
expect(selectors.namespaceSelector()).toBeInTheDocument();
});
//E
await userEvent.type(selectors.namespaceSelector(), namespace);
await userEvent.type(selectors.regionSelector(), region);
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: 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
<LocationDetailsOracle {...editProps} onChange={(l) => (location = l)} />,
Dismissed Show dismissed Hide dismissed
{ wrapper: Wrapper },
);
//V
await waitFor(() => {
expect(selectors.namespaceSelector()).toHaveValue(namespace);
});
expect(selectors.regionSelector()).toHaveValue(region);
});
});
11 changes: 11 additions & 0 deletions src/react/locations/LocationDetails/storageOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -213,4 +214,14 @@ export const storageOptions: Record<LocationTypeKey, StorageOptionValues> = {
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',
},
};
4 changes: 3 additions & 1 deletion src/react/locations/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
);
};

Expand Down
7 changes: 5 additions & 2 deletions src/react/utils/storageOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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,
Expand All @@ -34,8 +35,8 @@
/**
* 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.
Expand Down Expand Up @@ -65,6 +66,8 @@
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')) {
Dismissed Show dismissed Hide dismissed
return ORACLE_CLOUD_LOCATION_KEY;
} else {
return 'locationType' in location
? location.locationType
Expand Down
4 changes: 3 additions & 1 deletion src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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'
Expand Down
Loading