diff --git a/deploy/README.md b/deploy/README.md index 9cfb25f..1e91b6e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -8,6 +8,58 @@ The React application is deployed using AWS CloudFront and S3, utilizing a custo For each account, the React assets are built and then pushed to a designated S3 bucket using AWS CodeBuild and Lambda functions. Specifically, a Lambda function uploads the assets to S3 and subsequently triggers CodeBuild. CodeBuild then compiles the React application and uploads the built assets back to S3. +## Deployment Strategy + +The deployment strategy is to deploy the React application to the toolchain account, and then use AWS CodePipeline to deploy the application to the respective accounts. +![ORCA UI DUAL PIPELINE](../docs/orca-ui-dual-pipeline.png) + +## env config lambda + +The env config lambda is used to update the env.js file in the S3 bucket. Env Config Lmabda [here](./lambda/env_config_and_cdn_refresh.py) + +Normally, the lambda function is invoked by the CodeBuild project. This is done by adding a code build action in the CodeBuild project to invoke the lambda function. + +If you want to invoke the lambda function manually, you can use the following command (without payload): + +```sh +aws lambda invoke \ + --function-name CodeBuildEnvConfigLambdaBeta \ + response.json +``` + +if you wanna invoke manually with payload to update the api version, you can use the following command: + +```sh +aws lambda invoke \ + --function-name CodeBuildEnvConfigLambdaBeta \ + --payload '{"metadata_api_version": "v2"}' \ + response.json +``` + +Update multiple API versions + +```sh +aws lambda invoke \ + --function-name CodeBuildEnvConfigLambdaBeta \ + --payload '{ + "metadata_api_version": "v2", + "workflow_api_version": "v2", + "sequence_run_api_version": "v1", + "file_api_version": "v2" + }' \ + response.json +``` + +invoke with a specific AWS profile: + +```sh +aws lambda invoke \ + --profile your-profile-name \ + --function-name CodeBuildEnvConfigLambdaBeta \ + --payload '{"metadata_api_version": "v2"}' \ + response.json +``` + ## Development Change to the deploy directory diff --git a/deploy/config.ts b/deploy/config.ts index 7916661..a95c560 100644 --- a/deploy/config.ts +++ b/deploy/config.ts @@ -23,9 +23,9 @@ export const cloudFrontBucketNameConfig: Record = { }; export const configLambdaNameConfig: Record = { - [AppStage.BETA]: 'TriggerCodeBuildLambdaBeta', - [AppStage.GAMMA]: 'TriggerCodeBuildLambdaGamma', - [AppStage.PROD]: 'TriggerCodeBuildLambdaProd', + [AppStage.BETA]: 'CodeBuildEnvConfigLambdaBeta', + [AppStage.GAMMA]: 'CodeBuildEnvConfigLambdaGamma', + [AppStage.PROD]: 'CodeBuildEnvConfigLambdaProd', }; export const getAppStackConfig = (appStage: AppStage): ApplicationStackProps => { diff --git a/deploy/lambda/env_config.py b/deploy/lambda/env_config.py deleted file mode 100644 index 9704fd7..0000000 --- a/deploy/lambda/env_config.py +++ /dev/null @@ -1,71 +0,0 @@ - -import os -import boto3 -import json - -ssm = boto3.client('ssm') -s3 = boto3.client('s3') -cloudfront = boto3.client('cloudfront') - -def get_ssm_parameter(name, with_decryption=False): - try: - response = ssm.get_parameter(Name=name, WithDecryption=with_decryption) - return response['Parameter']['Value'] - except ssm.exceptions.ParameterNotFound: - print(f"SSM Parameter not found: {name}") - return None - except Exception as e: - print(f"Error fetching SSM parameter {name}: {str(e)}") - return None - -def handler(event, context): - - bucket_name = os.environ['BUCKET_NAME'] - cloudfront_distribution_id = os.environ['CLOUDFRONT_DISTRIBUTION_ID'] - - # List of SSM parameters to fetch - env_vars = { - 'VITE_COG_APP_CLIENT_ID': get_ssm_parameter(os.environ['VITE_COG_APP_CLIENT_ID']), - 'VITE_OAUTH_REDIRECT_IN': get_ssm_parameter(os.environ['VITE_OAUTH_REDIRECT_IN']), - 'VITE_OAUTH_REDIRECT_OUT': get_ssm_parameter(os.environ['VITE_OAUTH_REDIRECT_OUT']), - 'VITE_COG_USER_POOL_ID': get_ssm_parameter(os.environ['VITE_COG_USER_POOL_ID']), - 'VITE_COG_IDENTITY_POOL_ID': get_ssm_parameter(os.environ['VITE_COG_IDENTITY_POOL_ID']), - 'VITE_OAUTH_DOMAIN': get_ssm_parameter(os.environ['VITE_OAUTH_DOMAIN']), - 'VITE_UNSPLASH_CLIENT_ID': get_ssm_parameter(os.environ['VITE_UNSPLASH_CLIENT_ID'], with_decryption=True), - - 'VITE_REGION': os.environ['VITE_REGION'], - 'VITE_METADATA_URL': os.environ['VITE_METADATA_URL'], - 'VITE_WORKFLOW_URL': os.environ['VITE_WORKFLOW_URL'], - 'VITE_SEQUENCE_RUN_URL': os.environ['VITE_SEQUENCE_RUN_URL'], - 'VITE_FILE_URL': os.environ['VITE_FILE_URL'], - } - - env_js_content = f"window.config = {json.dumps(env_vars, indent=2)}" - - try: - s3.put_object(Bucket=bucket_name, Key='env.js', Body=env_js_content, ContentType='text/javascript') - - # invalidate cloudfront distribution for all files (clear cache) - cloudfront.create_invalidation( - DistributionId=cloudfront_distribution_id, - InvalidationBatch={ - 'Paths': { - 'Quantity': 1, - 'Items': ['/*'] - }, - 'CallerReference': str(context.aws_request_id) - } - ) - - return { - 'statusCode': 200, - 'body': f" env.js uploaded to {bucket_name}, and CloudFront cache invalidated for {cloudfront_distribution_id}" - } - except Exception as e: - # Log the error and return a failure response - print("Error:") - print(e) - return { - 'statusCode': 500, - 'body': f"Failed to upload env.js to {bucket_name}. {e}" - } diff --git a/deploy/lambda/env_config_and_cdn_refresh.py b/deploy/lambda/env_config_and_cdn_refresh.py new file mode 100644 index 0000000..548f02e --- /dev/null +++ b/deploy/lambda/env_config_and_cdn_refresh.py @@ -0,0 +1,108 @@ + +import os +import boto3 +import json + +ssm = boto3.client('ssm') +s3 = boto3.client('s3') +cloudfront = boto3.client('cloudfront') + +def get_ssm_parameter(name, with_decryption=False): + """Fetch a SSM parameter""" + try: + response = ssm.get_parameter(Name=name, WithDecryption=with_decryption) + return response['Parameter']['Value'] + except Exception as e: + print(f"Error fetching SSM parameter {name}: {e}") + return None + + +def update_api_versions(event): + """Update API versions from event with validation and logging""" + if not event or not isinstance(event, dict): + print("No update event data received, skipping API version updates") + return + + api_version_mappings = { + 'metadata_api_version': 'VITE_METADATA_API_VERSION', + 'workflow_api_version': 'VITE_WORKFLOW_API_VERSION', + 'sequence_run_api_version': 'VITE_SEQUENCE_RUN_API_VERSION', + 'file_api_version': 'VITE_FILE_API_VERSION' + } + + # Check if any version keys exist in the event + if not any(key in event for key in api_version_mappings): + print("No API version updates found in event") + return + + # update the environment variables + for event_key, env_key in api_version_mappings.items(): + version = event.get(event_key) + if version and isinstance(version, str): + os.environ[env_key] = version + print(f"Updated {env_key} to {version}") + +def handler(event, context): + """Handler for the lambda function""" + + # read the event to update the api version + update_api_versions(event) + + bucket_name = os.environ['BUCKET_NAME'] + cloudfront_distribution_id = os.environ['CLOUDFRONT_DISTRIBUTION_ID'] + + # List of SSM parameters to fetch + env_vars = { + 'VITE_COG_APP_CLIENT_ID': get_ssm_parameter('/orcaui/cog_app_client_id_stage'), + 'VITE_OAUTH_REDIRECT_IN': get_ssm_parameter('/orcaui/oauth_redirect_in_stage'), + 'VITE_OAUTH_REDIRECT_OUT': get_ssm_parameter('/orcaui/oauth_redirect_out_stage'), + 'VITE_COG_USER_POOL_ID': get_ssm_parameter('/data_portal/client/cog_user_pool_id'), + 'VITE_COG_IDENTITY_POOL_ID': get_ssm_parameter('/data_portal/client/cog_identity_pool_id'), + 'VITE_OAUTH_DOMAIN': get_ssm_parameter('/data_portal/client/oauth_domain'), + 'VITE_UNSPLASH_CLIENT_ID': get_ssm_parameter('/data_portal/unsplash/client_id'), + + 'VITE_REGION': os.environ['VITE_REGION'], + 'VITE_METADATA_URL': os.environ['VITE_METADATA_URL'], + 'VITE_WORKFLOW_URL': os.environ['VITE_WORKFLOW_URL'], + 'VITE_SEQUENCE_RUN_URL': os.environ['VITE_SEQUENCE_RUN_URL'], + 'VITE_FILE_URL': os.environ['VITE_FILE_URL'], + + # API Version + 'VITE_METADATA_API_VERSION': os.environ.get('VITE_METADATA_API_VERSION', None), + 'VITE_WORKFLOW_API_VERSION': os.environ.get('VITE_WORKFLOW_API_VERSION', None), + 'VITE_SEQUENCE_RUN_API_VERSION': os.environ.get('VITE_SEQUENCE_RUN_API_VERSION', None), + 'VITE_FILE_API_VERSION': os.environ.get('VITE_FILE_API_VERSION', None), + } + # Remove null values + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + env_js_content = f"window.config = {json.dumps(env_vars, indent=2)}" + + + try: + s3.put_object(Bucket=bucket_name, Key='env.js', Body=env_js_content, ContentType='text/javascript') + + # invalidate cloudfront distribution for all files (clear cache) + cloudfront.create_invalidation( + DistributionId=cloudfront_distribution_id, + InvalidationBatch={ + 'Paths': { + 'Quantity': 1, + 'Items': ['/*'] + }, + 'CallerReference': str(context.aws_request_id) + } + ) + + return { + 'statusCode': 200, + 'body': f" env.js uploaded to {bucket_name}, and CloudFront cache invalidated for {cloudfront_distribution_id}" + } + except Exception as e: + # Log the error and return a failure response + print("Error:") + print(e) + return { + 'statusCode': 500, + 'body': f"Failed to upload env.js to {bucket_name}. {e}" + } diff --git a/deploy/lib/application-stack.ts b/deploy/lib/application-stack.ts index bcd164c..2d95313 100644 --- a/deploy/lib/application-stack.ts +++ b/deploy/lib/application-stack.ts @@ -57,7 +57,7 @@ export class ApplicationStack extends Stack { functionName: props.configLambdaName, code: Code.fromAsset(path.join(__dirname, '..', 'lambda')), timeout: Duration.minutes(10), - handler: 'env_config.handler', + handler: 'env_config_and_cdn_refresh.handler', logRetention: RetentionDays.ONE_WEEK, runtime: Runtime.PYTHON_3_12, architecture: Architecture.ARM_64, diff --git a/docs/orca-ui-dual-pipeline.png b/docs/orca-ui-dual-pipeline.png new file mode 100644 index 0000000..9e74c70 Binary files /dev/null and b/docs/orca-ui-dual-pipeline.png differ diff --git a/src/api/sequenceRun.ts b/src/api/sequenceRun.ts index e1f2d46..57d70ed 100644 --- a/src/api/sequenceRun.ts +++ b/src/api/sequenceRun.ts @@ -3,18 +3,27 @@ import createClient from 'openapi-fetch'; import type { paths, components } from './types/sequence-run'; import { useSuspenseQuery, useQuery } from '@tanstack/react-query'; import { authMiddleware, UseSuspenseQueryOptions, UseQueryOptions } from './utils'; +import { env } from '@/utils/commonUtils'; const client = createClient({ baseUrl: config.apiEndpoint.sequenceRun }); client.use(authMiddleware); +const apiVersion = env.VITE_SEQUENCE_RUN_API_VERSION; + +function getVersionedPath(path: K): K { + if (!apiVersion) return path; + return path.replace('/api/v1/', `/api/${apiVersion}/`) as K; +} + export function createSequenceRunFetchingHook(path: K) { return function ({ params, reactQuery }: UseSuspenseQueryOptions) { + const versionedPath = getVersionedPath(path); return useSuspenseQuery({ ...reactQuery, queryKey: [path, params], queryFn: async ({ signal }) => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.GET(path, { params, signal }); + const { data } = await client.GET(versionedPath, { params, signal }); return data; }, }); @@ -23,12 +32,13 @@ export function createSequenceRunFetchingHook(path: K) { export function createSequenceRunQueryHook(path: K) { return function ({ params, reactQuery }: UseQueryOptions) { + const versionedPath = getVersionedPath(path); return useQuery({ ...reactQuery, queryKey: [path, params], queryFn: async ({ signal }) => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.GET(path, { params, signal }); + const { data } = await client.GET(versionedPath, { params, signal }); return data; }, }); diff --git a/src/api/workflow.ts b/src/api/workflow.ts index c7b8d55..b1846b4 100644 --- a/src/api/workflow.ts +++ b/src/api/workflow.ts @@ -8,10 +8,18 @@ import { UseQueryOptions, UseMutationOptions, } from './utils'; +import { env } from '@/utils/commonUtils'; const client = createClient({ baseUrl: config.apiEndpoint.workflow }); client.use(authMiddleware); +const apiVersion = env.VITE_WORKFLOW_API_VERSION; + +function getVersionedPath(path: K): K { + if (!apiVersion) return path; + return path.replace('/api/v1/', `/api/${apiVersion}/`) as K; +} + export function createWorkflowFetchingHook< K extends keyof paths, M extends keyof paths[K] & 'get', @@ -26,12 +34,13 @@ export function createWorkflowFetchingHook< }: Omit, 'queryKey' | 'queryFn'> & { signal?: AbortSignal; }) { + const versionedPath = getVersionedPath(path); return useSuspenseQuery({ ...reactQuery, - queryKey: [path, params], + queryKey: [versionedPath, params], queryFn: async () => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.GET(path, { + const { data } = await client.GET(versionedPath, { params: params as ParamsOption, signal: signal, }); @@ -48,6 +57,7 @@ export function createWorkflowQueryHook< ? T : never, >(path: K) { + const versionedPath = getVersionedPath(path); return function ({ params, reactQuery, @@ -55,10 +65,10 @@ export function createWorkflowQueryHook< }: Omit, 'queryKey' | 'queryFn'> & { signal?: AbortSignal }) { return useQuery({ ...reactQuery, - queryKey: [path, params], + queryKey: [versionedPath, params], queryFn: async ({ signal: querySignal }) => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.GET(path, { + const { data } = await client.GET(versionedPath, { params: params as ParamsOption, signal: signal || querySignal, }); @@ -70,11 +80,12 @@ export function createWorkflowQueryHook< export function createWorkflowPostMutationHook(path: K) { return function ({ params, reactQuery, body }: UseMutationOptions) { + const versionedPath = getVersionedPath(path); return useMutation({ ...reactQuery, mutationFn: async () => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.POST(path, { params, body: body }); + const { data } = await client.POST(versionedPath, { params, body: body }); return data; }, }); @@ -83,11 +94,12 @@ export function createWorkflowPostMutationHook(path: K) { export function createWorkflowPatchMutationHook(path: K) { return function ({ params, reactQuery, body }: UseMutationOptions) { + const versionedPath = getVersionedPath(path); return useMutation({ ...reactQuery, mutationFn: async () => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.PATCH(path, { params, body: body }); + const { data } = await client.PATCH(versionedPath, { params, body: body }); return data; }, }); @@ -96,11 +108,12 @@ export function createWorkflowPatchMutationHook(path: K) export function createWorkflowDeleteMutationHook(path: K) { return function ({ params, reactQuery, body }: UseMutationOptions) { + const versionedPath = getVersionedPath(path); return useMutation({ ...reactQuery, mutationFn: async () => { // @ts-expect-error: params is dynamic type type for openapi-fetch - const { data } = await client.DELETE(path, { params, body: body }); + const { data } = await client.DELETE(versionedPath, { params, body: body }); return data; }, }); diff --git a/start.sh b/start.sh index aeb13f7..d832160 100644 --- a/start.sh +++ b/start.sh @@ -81,6 +81,12 @@ export VITE_WORKFLOW_URL=${VITE_WORKFLOW_MANAGER_URL:-"http://localhost:8200"} export VITE_SEQUENCE_RUN_URL=${VITE_SEQUENCE_RUN_MANAGER_URL:-"http://localhost:8300"} export VITE_FILE_URL=${VITE_FILE_MANAGER_URL:-"http://localhost:8400"} +# API Version (default is v1, update this to update the api version respectively) +# export VITE_METADATA_API_VERSION=${VITE_METADATA_API_VERSION:-"v1"} +# export VITE_WORKFLOW_API_VERSION=${VITE_WORKFLOW_API_VERSION:-"v1"} +# export VITE_SEQUENCE_RUN_API_VERSION=${VITE_SEQUENCE_RUN_API_VERSION:-"v1"} +# export VITE_FILE_API_VERSION=${VITE_FILE_API_VERSION:-"v1"} + env | grep VITE yarn run -B vite