Skip to content

Commit

Permalink
Try out action and script
Browse files Browse the repository at this point in the history
  • Loading branch information
hdavey-gds committed Feb 20, 2024
1 parent f3cd45b commit 31590c4
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/add-quicksight-user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
type: choice
required: true
description: AWS Environment
options: [DEV, TEST, FEATURE, PRODUCTION]
options: [DEV, TEST, FEATURE, PRODUCTION, PRODUCTION-PREVIEW]
email:
type: string
required: true
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/quicksight-export.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: ✳️ Export analysis from Quicksight

on:
workflow_dispatch:
inputs:
environment:
type: choice
required: true
description: AWS Environment
options: [DEV, TEST, FEATURE, PRODUCTION, PRODUCTION-PREVIEW]
analysisId:
type: string
required: true
description: Id of the analysis to export (get from the url e.g. https://eu-west-2.quicksight.aws.amazon.com/sn/analyses/${analysisId})
bucketName:
type: string
required: false
description: Name of the S3 bucket to use for the export (defaults to \${lowercase_environment}-dap-quicksight-exports)
default: 'unset'
filename:
type: string
required: false
description: Filename of the export (defaults to export-\${analysis_id}-\${timestamp}.qs")
default: 'unset'

jobs:
invoke-quicksight-export-lambda:
# These permissions are needed to interact with GitHub's OIDC Token endpoint (enabling the aws-actions/configure-aws-credentials action)
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
# - name: Assume AWS quicksight import export lambdas role
# uses: aws-actions/configure-aws-credentials@v4
# with:
# aws-region: eu-west-2
# role-to-assume: ${{ secrets[format('QUICKSIGHT_IMPORT_EXPORT_LAMBDAS_INVOKE_ROLE_{0}', inputs.environment)] }}
- name: Invoke lambda
run: |
echo "Environment is ${{ inputs.environment }}"
PAYLOAD=$(scripts/quicksight-export-payload.sh -e ${{ inputs.environment }} -a ${{ inputs.analysisId }} -b ${{ inputs.bucketName }} -f ${{ inputs.filename }})
echo "$PAYLOAD" | jq
ENCODED=$(echo "$PAYLOAD" | openssl base64)
#aws --region eu-west-2 lambda invoke --function-name quicksight-add-users --payload "$ENCODED" out.json
#cat out.json
48 changes: 48 additions & 0 deletions scripts/quicksight-export-payload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash -e

usage_and_exit() {
echo "Usage: quicksight-export.sh -e ENVIRONMENT -a ANALYSIS_ID [-b BUCKET_NAME] [-f FILE_NAME] "
echo "Arguments:"
echo "-e environment quicksight environment (one of DEV, TEST, FEATURE, PRODUCTION or PRODUCTION-PREVIEW)"
echo "-a analysis id id of the analysis to export (uuid)"
echo "-b bucket name [optional] name of the S3 bucket to use for the export. defaults to \${lowercase_environment}-dap-quicksight-exports"
echo "-f file name [optional] filename of the export. defaults to export-\${analysis_id}-\${timestamp}.qs"
}

while getopts "e:a:b:f:" option; do
case "$option" in
e)
ENVIRONMENT="$OPTARG"
;;
a)
ANALYSIS_ID="$OPTARG"
;;
b)
BUCKET_NAME="$OPTARG"
;;
f)
FILE_NAME="$OPTARG"
;;
*)
echo "Unknown option $option"
usage_and_exit
;;
esac
done

if [ -z "$ENVIRONMENT" ] || [ -z "$ANALYSIS_ID" ]; then
echo -e "Error: Environment and/or analysis id not provided\n"
usage_and_exit
fi

LOWERCASE_ENVIRONMENT=$(echo "$ENVIRONMENT" | tr '[:upper:]' '[:lower:]')

if [ -z "$BUCKET_NAME" ] || [ "$BUCKET_NAME" == "unset" ]; then
BUCKET_NAME="$LOWERCASE_ENVIRONMENT-dap-quicksight-exports"
fi

if [ -z "$FILE_NAME" ] || [ "$FILE_NAME" == "unset" ]; then
FILE_NAME="export-$ANALYSIS_ID-$(date +"%Y-%m-%dT%H:%M:%S").qs"
fi

echo "{\"analysisId\":\"$ANALYSIS_ID\",\"bucketName\":\"$BUCKET_NAME\",\"filename\":\"$FILE_NAME\"}"
3 changes: 3 additions & 0 deletions src/handlers/quicksight-export/handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test('test', () => {
expect(2 + 2).toEqual(4);
});
105 changes: 105 additions & 0 deletions src/handlers/quicksight-export/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { getLogger } from '../../shared/powertools';
import { ensureDefined, getAccountId, sleep } from '../../shared/utils/utils';
import { quicksightClient, s3Client } from '../../shared/clients';
import { DescribeAssetBundleExportJobCommand, StartAssetBundleExportJobCommand } from '@aws-sdk/client-quicksight';
import type { DescribeAssetBundleExportJobCommandOutput } from '@aws-sdk/client-quicksight';
import type { Context } from 'aws-lambda';
import { PutObjectCommand } from '@aws-sdk/client-s3';

const logger = getLogger('lambda/quicksight-export');

interface QuicksightExportEvent {
analysisId: string;
bucketName: string;
filename: string;
}

export const handler = async (event: QuicksightExportEvent, context: Context): Promise<void> => {
try {
const accountId = getAccountId(context);
logger.info('Starting quicksight export', { event });
const jobId = await startExportJob(event, accountId);
const downloadUrl = await waitForExportToFinish(jobId, accountId);
await uploadToS3(event, downloadUrl);
} catch (error) {
logger.error('Error in quicksight export', { error });
throw error;
}
};

const startExportJob = async (event: QuicksightExportEvent, accountId: string): Promise<string> => {
const jobId = event.analysisId + Math.random().toString(36).substring(2);
const response = await quicksightClient.send(
new StartAssetBundleExportJobCommand({
AwsAccountId: accountId,
AssetBundleExportJobId: jobId,
ResourceArns: [event.analysisId],
ExportFormat: 'QUICKSIGHT_JSON',
IncludeAllDependencies: false,
IncludePermissions: true,
}),
);
if (response.Status === undefined || !response.Status.toString().startsWith('2')) {
throw new Error(`Start export job request with id ${jobId} returned status code of ${response.Status}`);
}
logger.info(`Export started with id ${jobId}`);
return jobId;
};

const waitForExportToFinish = async (jobId: string, accountId: string): Promise<string> => {
let describeResponse: DescribeAssetBundleExportJobCommandOutput | undefined;
const timeoutMs = 120 * 1000;
let timeRemaining = timeoutMs;
while (timeRemaining > 0) {
describeResponse = await describeExportJob(jobId, accountId);
// return if SUCCESSFUL to stop waiting, break if FAILED to allow error to be thrown
// other possible states are QUEUED_FOR_IMMEDIATE_EXECUTION and IN_PROGRESS where we do nothing as we want to continue waiting
if (describeResponse.JobStatus === 'SUCCESSFUL') {
return ensureDefined(() => describeResponse?.DownloadUrl);
} else if (describeResponse.JobStatus === 'FAILED') {
break;
}
timeRemaining -= 200;
await sleep(200);
}
logger.error('Export job did not complete', {
warnings: describeResponse?.Warnings,
errors: describeResponse?.Errors,
status: describeResponse?.JobStatus,
});
throw new Error(
`Export job did not complete in ${timeoutMs}ms - final status was ${JSON.stringify(describeResponse?.JobStatus)}`,
);
};

const describeExportJob = async (
exportJobId: string,
accountId: string,
): Promise<DescribeAssetBundleExportJobCommandOutput> => {
try {
return await quicksightClient.send(
new DescribeAssetBundleExportJobCommand({
AssetBundleExportJobId: exportJobId,
AwsAccountId: accountId,
}),
);
} catch (error) {
logger.error('Error checking status of export job', { error });
throw error;
}
};

const uploadToS3 = async (event: QuicksightExportEvent, downloadUrl: string): Promise<void> => {
try {
await s3Client.send(
new PutObjectCommand({
Bucket: event.bucketName,
Key: event.filename,
Body: await fetch(downloadUrl).then(response => response.body ?? undefined),
}),
);
} catch (error) {
logger.error('Error uploading export bundle to S3', { error });
throw error;
}
};
3 changes: 3 additions & 0 deletions src/handlers/quicksight-import/handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test('test', () => {
expect(2 + 2).toEqual(4);
});
19 changes: 19 additions & 0 deletions src/handlers/quicksight-import/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getLogger } from '../../shared/powertools';

const logger = getLogger('lambda/quicksight-import');

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface QuicksightImportEvent {}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface QuicksightImportResult {}

export const handler = async (event: QuicksightImportEvent): Promise<QuicksightImportResult> => {
try {
logger.info('Starting quicksight import', { event });
return {};
} catch (error) {
logger.error('Error in quicksight import', { error });
throw error;
}
};

0 comments on commit 31590c4

Please sign in to comment.