Skip to content

Commit

Permalink
Object Browser: access S3 endpoint via console proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsomthd committed Jan 29, 2025
1 parent 01f914e commit 3c6b543
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 40 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ yarn test-cypress-headless

By default, it will look for Chrome in the system and use it, but if you want to use Firefox instead, set BRIDGE_E2E_BROWSER_NAME environment variable in your shell with the value firefox.

### NooBaa Object Browser setup

To run NooBaa Object Browser in development mode, do the following:

```
oc port-forward $(oc get pods -n openshift-storage | grep noobaa-endpoint | awk '{print $1}') 6001
CONSOLE_VERSION=4.18 BRIDGE_PLUGIN_PROXY='{"services":[{"consoleAPIPath":"/api/proxy/plugin/odf-console/s3/","endpoint":"http://localhost:6001"}]}' BRIDGE_PLUGINS='odf-console=http://localhost:9001' PLUGIN=odf yarn dev:c
```

To see the NooBaa S3 logs: `oc logs -f deploy/noobaa-endpoint`

### Debugging with VSCode

To debug with VSCode breakpoints, do the following:
Expand Down
11 changes: 7 additions & 4 deletions packages/mco/constants/url-paths.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {
CONSOLE_PROXY_ROOT_PATH,
MCO_PROXY_ROOT_PATH,
} from '@odf/shared/constants/common';

// ACM thanos proxy endpoint
export const ACM_ENDPOINT =
'/api/proxy/plugin/odf-multicluster-console/acm-thanos-querier';
export const ACM_ENDPOINT = `${MCO_PROXY_ROOT_PATH}/acm-thanos-querier`;
// ACM thanos dev endpoint
export const DEV_ACM_ENDPOINT = '/acm-thanos-querier';
// ACM application details page endpoint
export const applicationDetails =
'/multicloud/applications/details/:namespace/:name';
// ACM search api proxy endpoint
export const ACM_SEARCH_PROXY_ENDPOINT =
'/api/proxy/plugin/acm/console/multicloud/proxy/search';
export const ACM_SEARCH_PROXY_ENDPOINT = `${CONSOLE_PROXY_ROOT_PATH}/acm/console/multicloud/proxy/search`;
// MCO DR navigation item base route
export const DR_BASE_ROUTE = '/multicloud/data-services/disaster-recovery';
88 changes: 64 additions & 24 deletions packages/odf/components/s3-browser/noobaa-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ import * as React from 'react';
import { useSafeK8sGet } from '@odf/core/hooks';
import { useODFNamespaceSelector } from '@odf/core/redux';
import { StatusBox } from '@odf/shared/generic/status-box';
import { SecretModel, RouteModel } from '@odf/shared/models';
import { S3Commands } from '@odf/shared/s3';
import { SecretKind, K8sResourceKind } from '@odf/shared/types';
import { SecretModel } from '@odf/shared/models';
import {
ODF_S3_PROXY_PATH,
S3_INTERNAL_ENDPOINT_PORT,
S3_INTERNAL_ENDPOINT_PREFIX,
S3_INTERNAL_ENDPOINT_SUFFIX,
S3_LOCAL_ENDPOINT,
S3Commands,
} from '@odf/shared/s3';
import { SecretKind } from '@odf/shared/types';
import type { HttpRequest } from '@smithy/types';
import * as _ from 'lodash-es';
import {
NOOBAA_ADMIN_SECRET,
NOOBAA_S3_ROUTE,
NOOBAA_ACCESS_KEY_ID,
NOOBAA_SECRET_ACCESS_KEY,
} from '../../constants';

const getS3Url = (odfNamespace: string) => {
return new URL(
window.location.hostname.includes('localhost')
? S3_LOCAL_ENDPOINT
: `${S3_INTERNAL_ENDPOINT_PREFIX}${odfNamespace}${S3_INTERNAL_ENDPOINT_SUFFIX}`
);
};

type NoobaaS3ContextType = {
noobaaS3: S3Commands;
noobaaS3Route: string;
Expand All @@ -33,48 +48,73 @@ export const NoobaaS3Provider: React.FC<NoobaaS3ProviderType> = ({
loading,
error,
}) => {
const { isODFNsLoaded, odfNsLoadError } = useODFNamespaceSelector();
const { odfNamespace, isODFNsLoaded, odfNsLoadError } =
useODFNamespaceSelector();

const [secretData, secretLoaded, secretError] = useSafeK8sGet<SecretKind>(
SecretModel,
NOOBAA_ADMIN_SECRET
);

const [routeData, routeLoaded, routeError] = useSafeK8sGet<K8sResourceKind>(
RouteModel,
NOOBAA_S3_ROUTE
);

const s3Route = React.useRef<string>();

const [noobaaS3, noobaaS3Error]: [S3Commands, unknown] = React.useMemo(() => {
if (!_.isEmpty(secretData) && !_.isEmpty(routeData)) {
if (!_.isEmpty(secretData)) {
try {
s3Route.current = `https://${routeData.spec.host}`;
// We initially set the S3 endpoint (instead of the proxy endpoint) so the
// signature calculation is done correctly.
const s3Url = getS3Url(odfNamespace);
s3Route.current = s3Url.toString();
const accessKeyId = atob(secretData.data?.[NOOBAA_ACCESS_KEY_ID]);
const secretAccessKey = atob(
secretData.data?.[NOOBAA_SECRET_ACCESS_KEY]
);
const client = new S3Commands(
s3Route.current,
accessKeyId,
secretAccessKey
);

// We must include the port in the host header as the proxy does (it's omitted
// if the port is the protocol default port e.g. 443 for 'https').
// It must be done BEFORE signature calculation.
client.middlewareStack.add(
(next) => (args) => {
const request: Partial<HttpRequest> = args.request;
if (s3Url.protocol === 'https:') {
request.headers[
'host'
] = `${s3Url.hostname}:${S3_INTERNAL_ENDPOINT_PORT}`;
}
return next(args);
},
{ step: 'build' }
);

// We must redirect the request to the proxy AFTER the signature calculation.
client.middlewareStack.add(
(next) => (args) => {
const request: Partial<HttpRequest> = args.request;
request.protocol = window.location.protocol;
request.hostname = window.location.hostname;
request.port = Number(window.location.port);
request.path = `${ODF_S3_PROXY_PATH}${request.path}`;
return next(args);
},
{ step: 'finalizeRequest' }
);

return [
new S3Commands(s3Route.current, accessKeyId, secretAccessKey),
null,
];
return [client, null];
} catch (err) {
return [{} as S3Commands, err];
}
}
return [{} as S3Commands, null];
}, [secretData, routeData]);
}, [secretData, odfNamespace]);

const allLoaded =
isODFNsLoaded &&
secretLoaded &&
routeLoaded &&
!loading &&
!_.isEmpty(noobaaS3);
const anyError =
odfNsLoadError || secretError || routeError || noobaaS3Error || error;
isODFNsLoaded && secretLoaded && !loading && !_.isEmpty(noobaaS3);
const anyError = odfNsLoadError || secretError || noobaaS3Error || error;

const contextData = React.useMemo(() => {
return { noobaaS3, noobaaS3Route: s3Route.current };
Expand Down
13 changes: 5 additions & 8 deletions packages/odf/components/storage-consumers/onboarding-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@odf/core/constants';
import { StorageQuota } from '@odf/core/types';
import { isUnlimitedQuota, isValidQuota } from '@odf/core/utils';
import { FieldLevelHelp, ModalFooter } from '@odf/shared';
import { ODF_PROXY_ROOT_PATH, FieldLevelHelp, ModalFooter } from '@odf/shared';
import { ModalBody, ModalTitle } from '@odf/shared/generic/ModalTitle';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { ExternalLink, getLastLanguage } from '@odf/shared/utils';
Expand Down Expand Up @@ -78,13 +78,10 @@ export const ClientOnBoardingModal: ClientOnBoardingModalProps = ({

const generateToken = () => {
setInProgress(true);
consoleFetch(
'/api/proxy/plugin/odf-console/provider-proxy/onboarding-tokens',
{
method: 'post',
body: quota.value > 0 ? JSON.stringify(quota) : null,
}
)
consoleFetch(`${ODF_PROXY_ROOT_PATH}/provider-proxy/onboarding-tokens`, {
method: 'post',
body: quota.value > 0 ? JSON.stringify(quota) : null,
})
.then((response) => {
setInProgress(false);
if (!response.ok) {
Expand Down
5 changes: 3 additions & 2 deletions packages/odf/constants/attach-storage.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const expandStorageUXBackendEndpoint =
'/api/proxy/plugin/odf-console/provider-proxy/expandstorage';
import { ODF_PROXY_ROOT_PATH } from '@odf/shared/constants/common';

export const expandStorageUXBackendEndpoint = `${ODF_PROXY_ROOT_PATH}/provider-proxy/expandstorage`;
5 changes: 5 additions & 0 deletions packages/shared/src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export const NOOBA_EXTERNAL_PG_SECRET_NAME = 'noobaa-external-pg';
export const NOOBAA_EXTERNAL_PG_TLS_SECRET_NAME = 'noobaa-external-pg-tls';
export const PLUGIN_VERSION =
typeof process === 'undefined' ? undefined : process?.env?.PLUGIN_VERSION;

// Proxy.
export const CONSOLE_PROXY_ROOT_PATH = '/api/proxy/plugin';
export const ODF_PROXY_ROOT_PATH = `${CONSOLE_PROXY_ROOT_PATH}/odf-console`;
export const MCO_PROXY_ROOT_PATH = `${CONSOLE_PROXY_ROOT_PATH}/odf-multicluster-console`;
4 changes: 3 additions & 1 deletion packages/shared/src/hooks/custom-prometheus-poll/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const ROSA_PROXY_ENDPOINT = '/api/proxy/plugin/odf-console/rosa-prometheus';
import { ODF_PROXY_ROOT_PATH } from '@odf/shared/constants/common';

const ROSA_PROXY_ENDPOINT = `${ODF_PROXY_ROOT_PATH}/rosa-prometheus`;

export const usePrometheusBasePath = () =>
window.SERVER_FLAGS.branding === 'rosa' ? ROSA_PROXY_ENDPOINT : '';
14 changes: 13 additions & 1 deletion packages/shared/src/s3/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { ODF_S3_PROXY_PATH } from '@odf/shared/s3/constants';
import {
CreateBucket,
ListBuckets,
Expand Down Expand Up @@ -85,7 +86,18 @@ export class S3Commands extends S3Client {
this.send(new ListObjectVersionsCommand(input));

getSignedUrl: GetSignedUrl = (input, expiresIn) =>
getSignedUrl(this, new GetObjectCommand(input), { expiresIn });
getSignedUrl(this, new GetObjectCommand(input), { expiresIn }).then(
(url) => {
// We must set the proxy URL because the S3 client
// doesn't execute 'finalizeRequest' step for this action.
const proxyUrl = new URL(url);
proxyUrl.protocol = window.location.protocol;
proxyUrl.hostname = window.location.hostname;
proxyUrl.port = window.location.port;
proxyUrl.pathname = `${ODF_S3_PROXY_PATH}${proxyUrl.pathname}`;
return proxyUrl.toString();
}
);

getObject: GetObject = (input) => this.send(new GetObjectCommand(input));

Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/s3/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ODF_PROXY_ROOT_PATH } from '@odf/shared/constants/common';

export const ODF_S3_PROXY_PATH = `${ODF_PROXY_ROOT_PATH}/s3`;
export const S3_INTERNAL_ENDPOINT_PORT = 443;
export const S3_INTERNAL_ENDPOINT_PREFIX = 'https://s3.';
export const S3_INTERNAL_ENDPOINT_SUFFIX = '.svc.cluster.local';
export const S3_LOCAL_ENDPOINT = 'http://localhost:6001';
1 change: 1 addition & 0 deletions packages/shared/src/s3/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './commands';
export * from './constants';
export * from './types';

0 comments on commit 3c6b543

Please sign in to comment.