diff --git a/.github/workflows/dev-deploy.yaml b/.github/workflows/dev-deploy.yaml index 5729d54..8890565 100644 --- a/.github/workflows/dev-deploy.yaml +++ b/.github/workflows/dev-deploy.yaml @@ -11,18 +11,18 @@ permissions: contents: read jobs: - deploy_commit: + deploy-proxy: runs-on: ubuntu-latest - + environment: development steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-east-1 - role-to-assume: arn:aws:iam::417712557820:role/GithubDeployRole + role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -46,3 +46,30 @@ jobs: task-definition: ${{ steps.render-data-proxy-container.outputs.task-definition }} service: source-data-proxy cluster: SourceCooperative-Dev + + deploy-cdk: + runs-on: ubuntu-latest + environment: development + defaults: + run: + working-directory: ./deploy + steps: + - uses: actions/checkout@v4.2.2 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} + - uses: actions/setup-node@v4.4.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: ./deploy/package-lock.json + - run: npm install -g aws-cdk + - run: npm ci + - name: Set vars to env + env: + VARS_JSON: ${{ toJSON(vars) }} + run: | + echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV + - run: cdk deploy --all --require-approval never diff --git a/.github/workflows/prod-deploy.yaml b/.github/workflows/prod-deploy.yaml index 991ef20..9f58e81 100644 --- a/.github/workflows/prod-deploy.yaml +++ b/.github/workflows/prod-deploy.yaml @@ -10,9 +10,9 @@ permissions: contents: read jobs: - deploy_release: + deploy-proxy: runs-on: ubuntu-latest - + environment: production steps: - id: latest uses: thebritican/fetch-latest-release@a36ee8ee464da77ba3e499ed6b75e3530e10f9bc # v2.0.0 @@ -27,7 +27,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-west-2 - role-to-assume: arn:aws:iam::417712557820:role/PublishECRImages + role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -51,3 +51,32 @@ jobs: task-definition: ${{ steps.render-data-proxy-container.outputs.task-definition }} service: source-data-proxy cluster: SourceCooperative-Prod + + deploy-cdk: + runs-on: ubuntu-latest + environment: production + defaults: + run: + working-directory: ./deploy + steps: + - uses: actions/checkout@v4.2.2 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} + - uses: actions/setup-node@v4.4.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: ./deploy/package-lock.json + - run: npm install -g aws-cdk + - run: npm ci + - name: Set vars to env + env: + VARS_JSON: ${{ toJSON(vars) }} + run: | + echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV + - run: cdk deploy --all --require-approval never + env: + STAGE: prod diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..f60797b --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/deploy/.npmignore b/deploy/.npmignore new file mode 100644 index 0000000..c1d6d45 --- /dev/null +++ b/deploy/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/deploy/.nvmrc b/deploy/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/deploy/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..b88e456 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,5 @@ +# Deployment + +This directory contains deployment tooling to create and manage the AWS infrastructure for the Source Data Proxy. + +It embraces an "Infrastructure as Code" approach via [AWS CDK](https://docs.aws.amazon.com/cdk/). Deployments should be triggered via Github Actions. diff --git a/deploy/bin/deploy.ts b/deploy/bin/deploy.ts new file mode 100644 index 0000000..767ce43 --- /dev/null +++ b/deploy/bin/deploy.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { DataProxyStack } from "../lib/data-proxy-stack"; + +const stage = process.env.STAGE || "dev"; + +const vpcId = process.env.VPC_ID; +if (!vpcId) { + throw new Error("VPC_ID is not set"); +} + +const app = new cdk.App(); +new DataProxyStack(app, `DataProxy-${stage}`, { + vpcId, + proxyDomain: `vercel-api-${stage}.internal`, + env: { + account: process.env.AWS_ACCOUNT_ID, + region: process.env.AWS_REGION, + }, +}); diff --git a/deploy/cdk.json b/deploy/cdk.json new file mode 100644 index 0000000..8dbbaa8 --- /dev/null +++ b/deploy/cdk.json @@ -0,0 +1,99 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/deploy.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/core:explicitStackTags": true, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true + } +} diff --git a/deploy/lib/data-proxy-stack.ts b/deploy/lib/data-proxy-stack.ts new file mode 100644 index 0000000..6783da3 --- /dev/null +++ b/deploy/lib/data-proxy-stack.ts @@ -0,0 +1,22 @@ +import * as cdk from "aws-cdk-lib"; +import { aws_ec2 as ec2 } from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { VercelApiProxy } from "./vercel-api-proxy"; + +interface DataProxyStackProps extends cdk.StackProps { + vpcId: string; + proxyDomain: string; +} + +export class DataProxyStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: DataProxyStackProps) { + super(scope, id, props); + + const vpc = ec2.Vpc.fromLookup(this, "vpc", { vpcId: props.vpcId }); + + new VercelApiProxy(this, "vercel-api-proxy", { + vpc, + proxyDomain: props.proxyDomain, + }); + } +} diff --git a/deploy/lib/vercel-api-proxy.ts b/deploy/lib/vercel-api-proxy.ts new file mode 100644 index 0000000..c33ad4b --- /dev/null +++ b/deploy/lib/vercel-api-proxy.ts @@ -0,0 +1,100 @@ +import { + aws_ec2 as ec2, + aws_iam as iam, + aws_route53 as route53, +} from "aws-cdk-lib"; +import { Construct } from "constructs"; + +interface VercelApiProxyProps { + vpc: ec2.IVpc; + proxyDomain: string; +} + +export class VercelApiProxy extends Construct { + /** + * To work around Vercel's firewall, we must proxy all requests for the Proxy API through + * a Squid proxy. This will allow us to have a stable IP address for the Proxy API which + * we can add to the Vercel firewall's bypass list. This allows us to retain ephemeral IP + * addresses for the Proxy API and to avoid using other techniques like passing data + * through a NAT Gateway which would have considerable cost implications. + */ + constructor(scope: Construct, id: string, props: VercelApiProxyProps) { + super(scope, id); + + const proxyPort = 3128; + + // Create security group for the proxy + const proxySg = new ec2.SecurityGroup(this, "proxy-sg", { + vpc: props.vpc, + description: "Allow inbound from ECS for Squid proxy", + allowAllOutbound: true, + }); + + // Allow ECS (internal) traffic on port 3128 + proxySg.addIngressRule( + ec2.Peer.ipv4(props.vpc.vpcCidrBlock), + ec2.Port.tcp(proxyPort), + "Allow ECS to connect to Squid" + ); + + // Squid install and minimal config + const userData = ec2.UserData.forLinux(); + userData.addCommands( + "yum update -y", + "yum install -y squid", + + // Write squid.conf using heredoc + "cat <<'EOF' > /etc/squid/squid.conf", + `http_port ${proxyPort}`, + "acl all src 0.0.0.0/0", + "http_access allow all", + "EOF", + + // Enable and start Squid + "systemctl enable squid", + "systemctl restart squid" + ); + + // Enable SSM access for the EC2 instance + const ssmRole = new iam.Role(this, "ec2-ssm-role", { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "AmazonSSMManagedInstanceCore" + ), + ], + }); + + // Launch EC2 instance + const instance = new ec2.Instance(this, "squid-proxy", { + vpc: props.vpc, + role: ssmRole, + instanceType: ec2.InstanceType.of( + ec2.InstanceClass.T3, + ec2.InstanceSize.MICRO + ), + machineImage: ec2.MachineImage.latestAmazonLinux2023(), + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + securityGroup: proxySg, + userData, + disableApiTermination: true, + }); + + // Allocate and associate Elastic IP + const eip = new ec2.CfnEIP(this, "proxy-eip", {}); + new ec2.CfnEIPAssociation(this, "proxy-eip-assoc", { + allocationId: eip.attrAllocationId, + instanceId: instance.instanceId, + }); + + // Route 53 Private Hosted Zone + const zone = new route53.PrivateHostedZone(this, "proxy-zone", { + vpc: props.vpc, + zoneName: props.proxyDomain, + }); + new route53.ARecord(this, "proxy-a-record", { + zone, + target: route53.RecordTarget.fromIpAddresses(instance.instancePrivateIp), + }); + } +} diff --git a/deploy/package-lock.json b/deploy/package-lock.json new file mode 100644 index 0000000..788c7e3 --- /dev/null +++ b/deploy/package-lock.json @@ -0,0 +1,678 @@ +{ + "name": "deploy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deploy", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.206.0", + "constructs": "^10.0.0" + }, + "bin": { + "deploy": "bin/deploy.js" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1023.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "45.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-45.2.0.tgz", + "integrity": "sha512-5TTUkGHQ+nfuUGwKA8/Yraxb+JdNUh4np24qk/VHXmrCMq+M6HfmGWfhcg/QlHA2S5P3YIamfYHdQAB4uSNLAg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1023.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1023.0.tgz", + "integrity": "sha512-DWMA+IrAsBUNF2RvH7ujpDp7wSJkqTkRL8yfK4AYpEjoGY1KMaKIfxz3M3+Nk3ogM7VhZiW3OGWEOgyDF47HOQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.206.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.206.0.tgz", + "integrity": "sha512-WQGSSzSX+CvIG3j4GICxCAARGaB2dbB2ZiAn8dqqWdUkF6G9pedlSd3bjB0NHOqrxJMu3jYQCYf3gLYTaJuR8A==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^45.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/deploy/package.json b/deploy/package.json new file mode 100644 index 0000000..2b902bc --- /dev/null +++ b/deploy/package.json @@ -0,0 +1,22 @@ +{ + "name": "deploy", + "version": "0.1.0", + "bin": { + "deploy": "bin/deploy.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1023.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.206.0", + "constructs": "^10.0.0" + } +} diff --git a/deploy/tsconfig.json b/deploy/tsconfig.json new file mode 100644 index 0000000..28bb557 --- /dev/null +++ b/deploy/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/src/apis/source.rs b/src/apis/source.rs index 0d03045..091dcad 100644 --- a/src/apis/source.rs +++ b/src/apis/source.rs @@ -20,6 +20,7 @@ pub struct SourceApi { data_connection_cache: Arc>, api_key_cache: Arc>, permissions_cache: Arc>>, + proxy_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -155,25 +156,6 @@ pub struct SourceProductList { pub next: Option, } -fn source_api_headers() -> reqwest::header::HeaderMap { - const CORE_REQUEST_HEADERS: &[(&str, &str)] = &[ - ("accept", "application/json"), - ( - "user-agent", - concat!("source-proxy/", env!("CARGO_PKG_VERSION")), - ), - ]; - CORE_REQUEST_HEADERS - .iter() - .map(|(name, value)| { - ( - reqwest::header::HeaderName::from_lowercase(name.as_bytes()).unwrap(), - reqwest::header::HeaderValue::from_str(value).unwrap(), - ) - }) - .collect() -} - #[async_trait] impl Api for SourceApi { /// Creates and returns a backend client for a specific repository. @@ -303,9 +285,9 @@ impl Api for SourceApi { account_id: String, user_identity: UserIdentity, ) -> Result { - let client = reqwest::Client::new(); + let client = self.build_req_client(); // Create headers - let mut headers = source_api_headers(); + let mut headers = self.build_source_headers(); if user_identity.api_key.is_some() { let api_key = user_identity.api_key.unwrap(); headers.insert( @@ -340,7 +322,7 @@ impl Api for SourceApi { } impl SourceApi { - pub fn new(endpoint: String) -> Self { + pub fn new(endpoint: String, proxy_url: Option) -> Self { let product_cache = Arc::new( Cache::builder() .time_to_live(Duration::from_secs(60)) // Set TTL to 60 seconds @@ -371,9 +353,47 @@ impl SourceApi { data_connection_cache, api_key_cache, permissions_cache, + proxy_url, } } + /// Creates a new `reqwest::Client` with the appropriate proxy settings. + /// + /// # Returns + /// + /// Returns a `reqwest::Client` with the appropriate proxy settings. + fn build_req_client(&self) -> reqwest::Client { + let mut client = reqwest::Client::builder(); + if let Some(proxy) = &self.proxy_url { + client = client.proxy(reqwest::Proxy::all(proxy).unwrap()); + } + client.build().unwrap() + } + + /// Builds the headers for the Source API. + /// + /// # Returns + /// + /// Returns a `reqwest::header::HeaderMap` with the appropriate headers. + fn build_source_headers(&self) -> reqwest::header::HeaderMap { + const CORE_REQUEST_HEADERS: &[(&str, &str)] = &[ + ("accept", "application/json"), + ( + "user-agent", + concat!("source-proxy/", env!("CARGO_PKG_VERSION")), + ), + ]; + CORE_REQUEST_HEADERS + .iter() + .map(|(name, value)| { + ( + reqwest::header::HeaderName::from_lowercase(name.as_bytes()).unwrap(), + reqwest::header::HeaderValue::from_str(value).unwrap(), + ) + }) + .collect() + } + /// Retrieves the repository record for a given account and repository ID. /// /// # Arguments @@ -402,8 +422,8 @@ impl SourceApi { "{}/api/v1/repositories/{}/{}", self.endpoint, account_id, repository_id ); - let client = reqwest::Client::new(); - let headers = source_api_headers(); + let client = self.build_req_client(); + let headers = self.build_source_headers(); let response = client.get(url).headers(headers).send().await?; let repository = process_json_response::(response, BackendError::RepositoryNotFound) @@ -421,8 +441,8 @@ impl SourceApi { data_connection_id: &str, ) -> Result { let source_key = env::var("SOURCE_KEY").unwrap(); - let client = reqwest::Client::new(); - let mut headers = source_api_headers(); + let client = self.build_req_client(); + let mut headers = self.build_source_headers(); headers.insert( reqwest::header::AUTHORIZATION, reqwest::header::HeaderValue::from_str(&source_key).unwrap(), @@ -486,12 +506,12 @@ impl SourceApi { } async fn fetch_api_key(&self, access_key_id: String) -> Result { - let client = reqwest::Client::new(); + let client = self.build_req_client(); let source_key = env::var("SOURCE_KEY").unwrap(); let source_api_url = env::var("SOURCE_API_URL").unwrap(); // Create headers - let mut headers = source_api_headers(); + let mut headers = self.build_source_headers(); headers.insert( reqwest::header::AUTHORIZATION, reqwest::header::HeaderValue::from_str(&source_key).unwrap(), @@ -567,11 +587,11 @@ impl SourceApi { account_id: &str, repository_id: &str, ) -> Result, BackendError> { - let client = reqwest::Client::new(); + let client = self.build_req_client(); let source_api_url = env::var("SOURCE_API_URL").unwrap(); // Create headers - let mut headers = source_api_headers(); + let mut headers = self.build_source_headers(); if user_identity.api_key.is_some() { let api_key = user_identity.api_key.unwrap(); headers.insert( diff --git a/src/main.rs b/src/main.rs index 0a288e3..e83fd08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -456,8 +456,9 @@ async fn index() -> impl Responder { // Main function to set up and run the HTTP server #[actix_web::main] async fn main() -> std::io::Result<()> { - let source_api_url = env::var("SOURCE_API_URL").unwrap(); - let source_api = web::Data::new(SourceApi::new(source_api_url)); + let source_api_url = env::var("SOURCE_API_URL").expect("SOURCE_API_URL must be set"); + let proxy_url = env::var("SOURCE_API_PROXY_URL").ok(); // Optional proxy for the Source API + let source_api = web::Data::new(SourceApi::new(source_api_url, proxy_url)); env_logger::init_from_env(Env::default().default_filter_or("info")); HttpServer::new(move || {